From a95bb068f951ae4fc8789fb92e94e72a50ac77e3 Mon Sep 17 00:00:00 2001 From: Iain McGinniss <309153+iainmcgin@users.noreply.github.com> Date: Sun, 24 May 2026 02:53:12 +0000 Subject: [PATCH] codegen: idiomatic UpperCamelCase enum aliases Protobuf enum values are SHOUTY_SNAKE_CASE (e.g. RULE_LEVEL_HIGH), which is unidiomatic in Rust. Those names remain the definitive variants, but codegen now additionally emits associated consts with the enum-name prefix stripped and the name converted to UpperCamelCase (RuleLevel::High), so downstream code can use idiomatic names. The aliases are purely additive: existing references and Debug output are unchanged, and the aliases work in pattern position with exhaustiveness checking preserved. The conversion is lossy, so two values can collide (FOO_BAR and FOO__BAR both map to FooBar). The rule is all-or-nothing per enum: if any two values collide after conversion, or a value would yield an invalid identifier, no aliases are emitted for that enum. A structured CodeGenWarning (surfaced as a build warning) and an enum doc note explain why, naming the exact values. This guarantees a match is never forced to mix conventions. Enabled by default via the new idiomatic_enum_aliases config flag (and the buffa-build builder setter). Diagnostics are returned by the new generate_with_diagnostics; the existing generate() delegates and discards them. Regenerates the checked-in WKT, bootstrap descriptor, and logging-example code. Closes #13 --- buffa-build/src/lib.rs | 35 +- buffa-codegen/src/bin/gen_descriptor_types.rs | 8 +- buffa-codegen/src/bin/gen_wkt_types.rs | 8 +- buffa-codegen/src/context.rs | 27 ++ buffa-codegen/src/enumeration.rs | 196 +++++++++- buffa-codegen/src/idents.rs | 142 +++++++ buffa-codegen/src/lib.rs | 139 ++++++- buffa-codegen/src/tests/idiomatic_enums.rs | 347 ++++++++++++++++++ buffa-codegen/src/tests/mod.rs | 1 + .../google.protobuf.compiler.plugin.rs | 11 + .../generated/google.protobuf.descriptor.rs | 322 ++++++++++++++++ .../src/generated/google.protobuf.struct.rs | 5 + examples/logging/src/gen/log.v1.log.rs | 20 + 13 files changed, 1250 insertions(+), 11 deletions(-) create mode 100644 buffa-codegen/src/tests/idiomatic_enums.rs diff --git a/buffa-build/src/lib.rs b/buffa-build/src/lib.rs index 72010b9..e828e67 100644 --- a/buffa-build/src/lib.rs +++ b/buffa-build/src/lib.rs @@ -32,7 +32,8 @@ use std::process::Command; use buffa::Message; use buffa_codegen::generated::descriptor::FileDescriptorSet; -use buffa_codegen::CodeGenConfig; +#[doc(inline)] +pub use buffa_codegen::CodeGenConfig; #[doc(inline)] pub use buffa_codegen::StringRepr; @@ -311,6 +312,24 @@ impl Config { self } + /// Enable or disable idiomatic `UpperCamelCase` enum aliases (matches the + /// [`CodeGenConfig`] default, currently on). + /// + /// Protobuf enum values are `SHOUTY_SNAKE_CASE` and stay the definitive Rust + /// variants. When enabled, codegen additionally emits associated `const`s + /// with the enum-name prefix stripped and the name converted to + /// `UpperCamelCase` (`RULE_LEVEL_HIGH` → `RuleLevel::High`), purely + /// additively — existing references and `Debug` output are unchanged. + /// + /// Aliases are suppressed per enum (with a build warning and a doc note) if + /// any two values would collide after conversion, so a match is never forced + /// to mix conventions. See [`CodeGenConfig::idiomatic_enum_aliases`]. + #[must_use] + pub fn idiomatic_enum_aliases(mut self, enabled: bool) -> Self { + self.codegen_config.idiomatic_enum_aliases = enabled; + self + } + /// Enable or disable unknown field preservation (default: true). /// /// When enabled (the default), unrecognized fields encountered during @@ -819,8 +838,18 @@ impl Config { // `.mod.rs` stitcher; only the stitchers need wiring into the // module tree (content files are reached via `include!` from // there). - let generated = - buffa_codegen::generate(&fds.file, &files_to_generate, &self.codegen_config)?; + let (generated, warnings) = buffa_codegen::generate_with_diagnostics( + &fds.file, + &files_to_generate, + &self.codegen_config, + )?; + + // Surface non-fatal codegen diagnostics as Cargo build warnings. This + // runs inside the consumer's `build.rs`, so `cargo:warning=` is shown in + // their normal `cargo build` output. + for warning in warnings { + println!("cargo:warning=buffa: {warning}"); + } // Write output files; collect (name, package) for PackageMod entries. let mut output_entries: Vec<(String, String)> = Vec::new(); diff --git a/buffa-codegen/src/bin/gen_descriptor_types.rs b/buffa-codegen/src/bin/gen_descriptor_types.rs index 4ecf7c3..05b91cd 100644 --- a/buffa-codegen/src/bin/gen_descriptor_types.rs +++ b/buffa-codegen/src/bin/gen_descriptor_types.rs @@ -61,8 +61,12 @@ fn main() { "google/protobuf/compiler/plugin.proto".to_string(), ]; - let generated = buffa_codegen::generate(&descriptor_set.file, &files_to_generate, &config) - .expect("code generation failed"); + let (generated, warnings) = + buffa_codegen::generate_with_diagnostics(&descriptor_set.file, &files_to_generate, &config) + .expect("code generation failed"); + for warning in &warnings { + eprintln!("warning: buffa: {warning}"); + } let out_dir = std::path::Path::new(&args[2]); fs::create_dir_all(out_dir).expect("failed to create output dir"); diff --git a/buffa-codegen/src/bin/gen_wkt_types.rs b/buffa-codegen/src/bin/gen_wkt_types.rs index cc5ca0a..c6456d9 100644 --- a/buffa-codegen/src/bin/gen_wkt_types.rs +++ b/buffa-codegen/src/bin/gen_wkt_types.rs @@ -100,8 +100,12 @@ fn main() { let files_to_generate: Vec = WKT_PROTOS.iter().map(|s| s.to_string()).collect(); - let generated = buffa_codegen::generate(&descriptor_set.file, &files_to_generate, &config) - .expect("code generation failed"); + let (generated, warnings) = + buffa_codegen::generate_with_diagnostics(&descriptor_set.file, &files_to_generate, &config) + .expect("code generation failed"); + for warning in &warnings { + eprintln!("warning: buffa: {warning}"); + } let out_dir = Path::new(&args[2]); fs::create_dir_all(out_dir).expect("failed to create output dir"); diff --git a/buffa-codegen/src/context.rs b/buffa-codegen/src/context.rs index a6b9cbb..fe8b3cd 100644 --- a/buffa-codegen/src/context.rs +++ b/buffa-codegen/src/context.rs @@ -90,6 +90,18 @@ pub struct CodeGenContext<'a> { /// the scope's occupied set. Entries exist for every top-level message; the /// value equals `snake_case(Name)` when no deconfliction was needed. nested_module_names: HashMap, + /// Non-fatal diagnostics accumulated during generation (e.g. an enum whose + /// idiomatic CamelCase aliases were suppressed by a naming conflict). + /// + /// Interior-mutable so deeply-nested codegen helpers can record a warning + /// through the shared `&CodeGenContext` without threading a sink through + /// every signature. Drained by [`generate_with_diagnostics`] after + /// generation; callers surface them as build warnings. The `RefCell` commits + /// the context to single-threaded use; switch to a `Mutex` if package + /// generation is ever parallelized. + /// + /// [`generate_with_diagnostics`]: crate::generate_with_diagnostics + warnings: std::cell::RefCell>, } /// The immediate child-package segment names directly under `package`. @@ -354,9 +366,24 @@ impl<'a> CodeGenContext<'a> { enum_closedness, comment_map, nested_module_names, + warnings: std::cell::RefCell::new(Vec::new()), } } + /// Record a non-fatal diagnostic to surface as a build warning. + pub(crate) fn warn(&self, warning: crate::CodeGenWarning) { + self.warnings.borrow_mut().push(warning); + } + + /// Drain the diagnostics accumulated during generation. + /// + /// `pub(crate)` so it can only be called from [`generate_with_diagnostics`] + /// after all packages are generated — draining mid-flight would truncate the + /// diagnostic stream. + pub(crate) fn take_warnings(&self) -> Vec { + self.warnings.take() + } + /// The nested-types module name for a top-level message, deconflicted /// against sub-package modules (issue #135). /// diff --git a/buffa-codegen/src/enumeration.rs b/buffa-codegen/src/enumeration.rs index df9af8d..6f31215 100644 --- a/buffa-codegen/src/enumeration.rs +++ b/buffa-codegen/src/enumeration.rs @@ -125,6 +125,13 @@ pub fn generate_enum( // fall back to the first primary variant. let mut zero_variant: Option = None; let mut first_variant: Option = None; + // Per-value records for idiomatic CamelCase alias generation, collected only + // when the feature is enabled. Each entry is + // `(proto_value_name, alias_target, own_ident_string)` where + // `own_ident_string` is the value's existing variant/alias identifier (which + // a CamelCase alias must not duplicate) and `alias_target` is the variant a + // generated `const` would point at. + let mut value_records: Vec<(String, Ident, String)> = Vec::new(); for v in &enum_desc.value { let value_name = v @@ -150,6 +157,13 @@ pub fn generate_enum( from_proto_name_arms.push(quote! { #value_name => ::core::option::Option::Some(Self::#primary_ident) }); + if ctx.config.idiomatic_enum_aliases { + value_records.push(( + value_name.to_string(), + primary_ident, + variant_ident.to_string(), + )); + } } else { seen.insert(number, value_name); if first_variant.is_none() { @@ -168,16 +182,31 @@ pub fn generate_enum( proto_name_arms.push(quote! { Self::#variant_ident => #value_name }); + if ctx.config.idiomatic_enum_aliases { + value_records.push(( + value_name.to_string(), + variant_ident.clone(), + variant_ident.to_string(), + )); + } value_idents.push(variant_ident); } } - let alias_block = if alias_consts.is_empty() { + // Idiomatic CamelCase aliases (feature-gated; `value_records` is empty when + // disabled). Returns the extra `const` items to emit and, when aliases are + // suppressed by a conflict, a doc note to append to the enum. + let enum_simple_name = enum_desc.name.as_deref().unwrap_or(rust_name); + let (idiomatic_consts, idiomatic_doc_note) = + idiomatic_aliases(ctx, rust_name, enum_simple_name, value_records); + + let alias_block = if alias_consts.is_empty() && idiomatic_consts.is_empty() { quote! {} } else { quote! { impl #name_ident { #(#alias_consts)* + #(#idiomatic_consts)* } } }; @@ -223,8 +252,11 @@ pub fn generate_enum( quote! {} }; - let enum_doc = - crate::comments::doc_attrs_resolved(ctx.comment(proto_fqn), proto_fqn, &ctx.type_map); + let enum_doc = { + let base = + crate::comments::doc_attrs_resolved(ctx.comment(proto_fqn), proto_fqn, &ctx.type_map); + quote! { #base #idiomatic_doc_note } + }; let custom_type_attrs = crate::context::CodeGenContext::matching_attributes( &ctx.config.type_attributes, proto_fqn, @@ -282,3 +314,161 @@ pub fn generate_enum( } }) } + +/// Compute idiomatic `UpperCamelCase` alias `const`s for an enum. +/// +/// `records` holds one entry per proto value (empty when the feature is +/// disabled): `(proto_value_name, alias_target, own_ident_string)`, where +/// `own_ident_string` is the value's existing variant/alias identifier (which a +/// CamelCase alias must not duplicate). The proto names stay the definitive +/// variants; this only adds aliases. +/// +/// Returns the `const` items to emit and a doc-note token stream (empty unless +/// aliases were suppressed). The rule is all-or-nothing per enum: if any two +/// values would collide after conversion — or a value would yield an invalid +/// identifier — no aliases are emitted, a [`CodeGenWarning`](crate::CodeGenWarning) +/// is recorded on `ctx`, and the returned doc note explains the suppression. This +/// guarantees a match is never forced to mix `SHOUTY_SNAKE_CASE` and idiomatic +/// names. +fn idiomatic_aliases( + ctx: &CodeGenContext, + rust_name: &str, + enum_simple_name: &str, + records: Vec<(String, Ident, String)>, +) -> (Vec, TokenStream) { + use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; + use std::fmt::Write; + + if records.is_empty() { + return (Vec::new(), quote! {}); + } + + let is_valid = |c: &str| !c.is_empty() && !c.starts_with(|ch: char| ch.is_ascii_digit()); + + let prefix = format!("{}_", crate::idents::to_shouty_snake_case(enum_simple_name)); + + // Strip the enum-name prefix only if *every* value carries it and stays a + // valid identifier afterwards; otherwise keep full names. Deciding this for + // the whole enum (not per value) keeps the result from mixing stripped and + // unstripped names. + let strip = records.iter().all(|(name, ..)| { + name.strip_prefix(&prefix) + .is_some_and(|base| is_valid(&crate::idents::to_upper_camel_case(base))) + }); + + let camel = |name: &str| { + let base = if strip { + name.strip_prefix(&prefix).unwrap_or(name) + } else { + name + }; + crate::idents::to_upper_camel_case(base) + }; + + // Identifiers already occupying the enum's type namespace (variants + proto + // `allow_alias` consts), guaranteed unique by protobuf, owned so `records` + // can be consumed below. + let existing: HashSet = records.iter().map(|(_, _, own)| own.clone()).collect(); + + // Group proto values by the escaped CamelCase identifier they would claim. + // A value also pulls in the owner of an existing variant/alias its CamelCase + // form lands on — that is the silent variant↔const shadow case. + let mut buckets: BTreeMap> = BTreeMap::new(); + let mut invalid: BTreeSet = BTreeSet::new(); + { + let owner: HashMap<&str, &str> = records + .iter() + .map(|(name, _, own)| (own.as_str(), name.as_str())) + .collect(); + for (name, _, _) in &records { + let candidate = camel(name); + if !is_valid(&candidate) { + // Defensive: unreachable under the current strip logic (the + // stripped path is taken only when every value stays valid, and + // the unstripped path uses raw proto names, which are never empty + // or digit-leading). Kept so a future converter that can emit an + // invalid identifier still suppresses rather than emits bad code. + invalid.insert(name.clone()); + continue; + } + let escaped = crate::idents::make_field_ident(&candidate).to_string(); + if let Some(&existing_owner) = owner.get(escaped.as_str()) { + buckets + .entry(escaped.clone()) + .or_default() + .insert(existing_owner.to_string()); + } + buckets.entry(escaped).or_default().insert(name.clone()); + } + } + + let conflicts: Vec<(&String, &BTreeSet)> = buckets + .iter() + .filter(|(_, claimants)| claimants.len() > 1) + .collect(); + + if conflicts.is_empty() && invalid.is_empty() { + // Clean: emit an alias for every value whose CamelCase form differs from + // its own variant/alias identifier (skipping the redundant ones, which + // are the only way an emitted name can already be in `existing`). + let consts = records + .into_iter() + .filter_map(|(name, target, _own)| { + let escaped = crate::idents::make_field_ident(&camel(&name)); + if existing.contains(&escaped.to_string()) { + return None; + } + // A short doc instead of duplicating the variant's proto comment: + // links the reader to the canonical variant and warns that + // `Debug` prints the variant name, not this alias. + let alias_doc = format!( + "Idiomatic alias for [`Self::{target}`]; `Debug` prints the variant name." + ); + Some(quote! { + #[doc = #alias_doc] + #[allow(non_upper_case_globals)] + pub const #escaped: Self = Self::#target; + }) + }) + .collect(); + return (consts, quote! {}); + } + + // Suppressed: record a structured warning and an enum doc note describing + // every clash, so the reason is visible both at build time and in docs. + let conflict_data: Vec = conflicts + .iter() + .map(|(camel_ident, claimants)| crate::AliasConflict { + camel_target: (*camel_ident).clone(), + proto_values: claimants.iter().cloned().collect(), + }) + .collect(); + let invalid_data: Vec = invalid.into_iter().collect(); + + // Build the doc note by borrowing, then hand both lists to `warn` by move. + let mut note = String::from( + "Idiomatic CamelCase aliases are not generated for this enum: two or more proto values \ + collide after conversion (or would be invalid identifiers). Use the `SHOUTY_SNAKE_CASE` \ + variants directly. Collisions:\n", + ); + for conflict in &conflict_data { + let joined = conflict + .proto_values + .iter() + .map(|n| format!("`{n}`")) + .collect::>() + .join(", "); + let _ = writeln!(note, "- {joined} → `{}`", conflict.camel_target); + } + for name in &invalid_data { + let _ = writeln!(note, "- `{name}` produces an invalid identifier"); + } + + ctx.warn(crate::CodeGenWarning::IdiomaticAliasesSuppressed { + enum_name: rust_name.to_string(), + conflicts: conflict_data, + invalid: invalid_data, + }); + + (Vec::new(), quote! { #[doc = #note] }) +} diff --git a/buffa-codegen/src/idents.rs b/buffa-codegen/src/idents.rs index 78ad9b1..d232b6e 100644 --- a/buffa-codegen/src/idents.rs +++ b/buffa-codegen/src/idents.rs @@ -76,6 +76,85 @@ pub fn make_field_ident(name: &str) -> Ident { } } +/// Convert a protobuf enum value name to `UpperCamelCase`. +/// +/// Word boundaries are underscores **and** case transitions, so the conversion +/// works on the canonical `SHOUTY_SNAKE_CASE` (`RULE_LEVEL_HIGH` → `RuleLevelHigh`) +/// as well as non-canonical mixed-case inputs: a lower→upper transition starts a +/// word (`myValue` → `MyValue`) and an acronym ends a word at the upper→lower +/// transition (`HTTPServer` → `HttpServer`). Each word's first character is +/// upper-cased and the rest lower-cased. +/// +/// The conversion is intentionally lossy: `FOO_BAR` and `FOO__BAR` both collapse +/// to `FooBar`, and `HTTPServer` and `HTTP_SERVER` both produce `HttpServer`. The +/// caller is responsible for detecting the resulting collisions. +/// +/// A leading digit in the output is only reachable when the caller has stripped +/// a prefix first (e.g. `VERSION_2` → `2`); it is preserved verbatim, so callers +/// that need a valid Rust identifier must check for it themselves. +#[must_use] +pub fn to_upper_camel_case(s: &str) -> String { + let chars: Vec = s.chars().collect(); + let mut out = String::new(); + let mut start_of_word = true; + for (i, &ch) in chars.iter().enumerate() { + if ch == '_' { + start_of_word = true; + continue; + } + // Within a run of non-underscore characters, detect a word boundary at + // case transitions so mixed-case input splits correctly. + if !start_of_word && i > 0 { + let prev = chars[i - 1]; + let lower_to_upper = prev.is_lowercase() && ch.is_uppercase(); + let acronym_end = prev.is_uppercase() + && ch.is_uppercase() + && chars.get(i + 1).is_some_and(|c| c.is_lowercase()); + if lower_to_upper || acronym_end { + start_of_word = true; + } + } + if start_of_word { + out.extend(ch.to_uppercase()); + start_of_word = false; + } else { + out.extend(ch.to_lowercase()); + } + } + out +} + +/// Convert a type name to `SHOUTY_SNAKE_CASE`. +/// +/// Used to reconstruct the conventional enum-value prefix from an enum's proto +/// name so it can be stripped: `RuleLevel` → `RULE_LEVEL` (then values like +/// `RULE_LEVEL_HIGH` lose the `RULE_LEVEL_` prefix). An underscore is inserted +/// at each lower→upper boundary and at acronym→word boundaries +/// (`HTTPServer` → `HTTP_SERVER`); existing underscores are preserved without +/// doubling. +#[must_use] +pub fn to_shouty_snake_case(s: &str) -> String { + let chars: Vec = s.chars().collect(); + let mut out = String::new(); + for (i, &ch) in chars.iter().enumerate() { + if ch == '_' { + out.push('_'); + continue; + } + if i > 0 && ch.is_uppercase() && chars[i - 1] != '_' { + let prev = chars[i - 1]; + let prev_starts_word = prev.is_lowercase() || prev.is_ascii_digit(); + let acronym_boundary = + prev.is_uppercase() && chars.get(i + 1).is_some_and(|c| c.is_lowercase()); + if prev_starts_word || acronym_boundary { + out.push('_'); + } + } + out.extend(ch.to_uppercase()); + } + out +} + /// Escape a proto package segment for use as a Rust `mod` name. /// /// Returns `r#` prefix for raw-able keywords, `_` suffix for path-position @@ -247,6 +326,69 @@ mod tests { assert_eq!(escape_mod_ident("super"), "super_"); } + #[test] + fn upper_camel_basic() { + assert_eq!(to_upper_camel_case("RULE_LEVEL_HIGH"), "RuleLevelHigh"); + assert_eq!(to_upper_camel_case("UNKNOWN"), "Unknown"); + assert_eq!(to_upper_camel_case("low_priority"), "LowPriority"); + assert_eq!(to_upper_camel_case("HTTP_SERVER"), "HttpServer"); + } + + #[test] + fn upper_camel_lossy_collisions() { + // Doubled and absent underscores collapse to the same identifier — the + // caller must detect this. + assert_eq!(to_upper_camel_case("FOO_BAR"), "FooBar"); + assert_eq!(to_upper_camel_case("FOO__BAR"), "FooBar"); + // Acronym vs snake also collapse — both must resolve to one identifier + // so the caller can detect the collision. + assert_eq!(to_upper_camel_case("HTTPServer"), "HttpServer"); + assert_eq!(to_upper_camel_case("HTTP_SERVER"), "HttpServer"); + } + + #[test] + fn upper_camel_mixed_case_input() { + // Case transitions are word boundaries, so an already-CamelCase value + // round-trips (and is later skipped as a redundant alias). + assert_eq!(to_upper_camel_case("MyValue"), "MyValue"); + assert_eq!(to_upper_camel_case("fooBar"), "FooBar"); + assert_eq!(to_upper_camel_case("Active"), "Active"); + } + + #[test] + fn upper_camel_digit_and_empty() { + // Reachable only after a prefix strip; preserved verbatim for the + // caller's validity check. + assert_eq!(to_upper_camel_case("2"), "2"); + assert_eq!(to_upper_camel_case(""), ""); + assert_eq!(to_upper_camel_case("FOO_2"), "Foo2"); + } + + #[test] + fn upper_camel_keyword_source() { + // `SELF` folds to the keyword `Self`; identifier escaping is the + // caller's job (via `make_field_ident`). + assert_eq!(to_upper_camel_case("SELF"), "Self"); + } + + #[test] + fn shouty_snake_basic() { + assert_eq!(to_shouty_snake_case("RuleLevel"), "RULE_LEVEL"); + assert_eq!(to_shouty_snake_case("NullValue"), "NULL_VALUE"); + assert_eq!(to_shouty_snake_case("Type"), "TYPE"); + } + + #[test] + fn shouty_snake_acronym() { + assert_eq!(to_shouty_snake_case("HTTPServer"), "HTTP_SERVER"); + } + + #[test] + fn shouty_snake_already_snakey() { + // Idempotent on names that already carry underscores. + assert_eq!(to_shouty_snake_case("RULE_LEVEL"), "RULE_LEVEL"); + } + #[test] fn keyword_coverage() { assert!(is_rust_keyword("type")); diff --git a/buffa-codegen/src/lib.rs b/buffa-codegen/src/lib.rs index f38afca..77a0b00 100644 --- a/buffa-codegen/src/lib.rs +++ b/buffa-codegen/src/lib.rs @@ -472,6 +472,36 @@ pub struct CodeGenConfig { /// /// Defaults to `false`. pub generate_reflection: bool, + /// Emit idiomatic `UpperCamelCase` constant aliases alongside each enum + /// variant. + /// + /// Protobuf style names enum values in `SHOUTY_SNAKE_CASE`, conventionally + /// prefixed with the enum name (`RULE_LEVEL_HIGH`). Those names remain the + /// definitive Rust variants — they are guaranteed unique and valid by + /// protobuf, and existing references (including `Debug` output) are + /// unchanged. When this is enabled, codegen additionally emits associated + /// `const`s with the prefix stripped and the name converted to + /// `UpperCamelCase` (`RULE_LEVEL_HIGH` → `High`), so downstream code can + /// write `RuleLevel::High`. + /// + /// The conversion is lossy, so two values can collide (`FOO_BAR` and + /// `FOO__BAR` both map to `FooBar`). The rule is all-or-nothing per enum: + /// if any two values would collide after conversion, or a value would yield + /// an invalid identifier, **no** aliases are emitted for that enum (a + /// [`CodeGenWarning`] and an enum doc note explain why). This keeps every + /// match either fully `SHOUTY_SNAKE_CASE` or fully idiomatic, never a forced + /// mix. + /// + /// The aliases are associated `const`s, which work in pattern position too: + /// a `match` written entirely against aliases is still exhaustiveness-checked + /// (the "non-exhaustive" error names the underlying `SHOUTY_SNAKE_CASE` + /// variant, since that is the canonical name). + /// + /// Defaults to `true`: the aliases are purely additive (the proto names + /// remain the variants, and `Debug` is unchanged), so enabling by default is + /// backward-compatible, and the all-or-nothing rule guarantees correctness on + /// any enum. + pub idiomatic_enum_aliases: bool, } impl Default for CodeGenConfig { @@ -496,6 +526,7 @@ impl Default for CodeGenConfig { gate_impls_on_crate_features: false, generate_with_setters: true, generate_reflection: false, + idiomatic_enum_aliases: true, } } } @@ -632,6 +663,79 @@ pub(crate) fn effective_file_extern_paths( .collect() } +/// One CamelCase collision: a target identifier and the proto value names that +/// would all convert onto it. +/// +/// Part of [`CodeGenWarning::IdiomaticAliasesSuppressed`]. +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub struct AliasConflict { + /// The `UpperCamelCase` identifier the colliding values map to. + pub camel_target: String, + /// The proto value names that convert onto `camel_target` (includes a + /// literal variant name when an alias would shadow it). + pub proto_values: Vec, +} + +/// A non-fatal diagnostic produced during code generation. +/// +/// Returned by [`generate_with_diagnostics`]. Render the human-readable form via +/// the [`Display`](core::fmt::Display) impl (e.g. `cargo:warning={warning}`), or +/// match on the variant for programmatic handling. The enum and its variants are +/// `#[non_exhaustive]` so new diagnostic kinds and fields can be added without a +/// breaking change. +#[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub enum CodeGenWarning { + /// Idiomatic CamelCase aliases were suppressed for an enum because two or + /// more proto values collide after conversion, or a value would convert to + /// an invalid identifier. The enum's `SHOUTY_SNAKE_CASE` variants are + /// unaffected. + #[non_exhaustive] + IdiomaticAliasesSuppressed { + /// The Rust name of the affected enum. + enum_name: String, + /// Each collision, by target identifier. Empty if the only problem was + /// invalid identifiers. + conflicts: Vec, + /// Proto values that would convert to an invalid Rust identifier. + invalid: Vec, + }, +} + +impl core::fmt::Display for CodeGenWarning { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::IdiomaticAliasesSuppressed { + enum_name, + conflicts, + invalid, + } => { + // Name the cause accurately: a collision, an invalid identifier, + // or both. + let cause = match (conflicts.is_empty(), invalid.is_empty()) { + (false, true) => "naming conflict", + (true, false) => "invalid identifier", + _ => "naming conflict / invalid identifier", + }; + write!( + f, + "enum `{enum_name}`: idiomatic CamelCase aliases suppressed ({cause})" + )?; + let mut parts: Vec = conflicts + .iter() + .map(|c| format!("{} → {}", c.proto_values.join(", "), c.camel_target)) + .collect(); + parts.extend(invalid.iter().map(|n| format!("{n} → invalid identifier"))); + if !parts.is_empty() { + write!(f, ": {}", parts.join("; "))?; + } + Ok(()) + } + } + } +} + /// Generate Rust source files from a set of file descriptors. /// /// `files_to_generate` is the set of file names that were explicitly requested @@ -643,11 +747,44 @@ pub(crate) fn effective_file_extern_paths( /// are omitted); each distinct package emits one `.mod.rs` /// stitcher. Packages are processed in sorted order for deterministic /// output. +/// +/// # Diagnostics +/// +/// Non-fatal diagnostics produced during generation (e.g. an enum whose +/// idiomatic CamelCase aliases were suppressed by a naming conflict) are +/// **discarded** here. Use [`generate_with_diagnostics`] to receive them and +/// surface them as build warnings. pub fn generate( file_descriptors: &[FileDescriptorProto], files_to_generate: &[String], config: &CodeGenConfig, ) -> Result, CodeGenError> { + Ok(generate_with_diagnostics(file_descriptors, files_to_generate, config)?.0) +} + +/// Like [`generate`], but also returns the non-fatal [`CodeGenWarning`]s +/// collected during generation (e.g. enums whose idiomatic CamelCase aliases +/// were suppressed by a naming conflict). +/// +/// Surface each warning via its [`Display`](core::fmt::Display) impl — e.g. as a +/// `cargo:warning=...` from a `build.rs`, or on stderr from a standalone +/// generator — or match on it for programmatic handling. [`generate`] discards +/// them, so existing callers are unaffected. +/// +/// Warnings are returned only on success. On error, any warnings already +/// collected are dropped along with the partial output — the [`CodeGenError`] +/// is the actionable signal. +/// +/// # Errors +/// +/// Returns [`CodeGenError::FileNotFound`] if a name in `files_to_generate` has +/// no matching descriptor, and other [`CodeGenError`] variants for malformed +/// descriptors (e.g. a missing required field) encountered while generating. +pub fn generate_with_diagnostics( + file_descriptors: &[FileDescriptorProto], + files_to_generate: &[String], + config: &CodeGenConfig, +) -> Result<(Vec, Vec), CodeGenError> { let ctx = context::CodeGenContext::for_generate(file_descriptors, files_to_generate, config); // Group requested files by package. BTreeMap → deterministic output order. @@ -677,7 +814,7 @@ pub fn generate( generate_package(&ctx, &package, &files, &fds_bytes, &mut output)?; } - Ok(output) + Ok((output, ctx.take_warnings())) } /// Generate a module tree that assembles per-package `.mod.rs` files into diff --git a/buffa-codegen/src/tests/idiomatic_enums.rs b/buffa-codegen/src/tests/idiomatic_enums.rs new file mode 100644 index 0000000..3124b1f --- /dev/null +++ b/buffa-codegen/src/tests/idiomatic_enums.rs @@ -0,0 +1,347 @@ +//! Idiomatic `UpperCamelCase` enum alias generation +//! ([`CodeGenConfig::idiomatic_enum_aliases`]). +//! +//! The proto `SHOUTY_SNAKE_CASE` names stay the definitive variants; these +//! tests cover the additive alias `const`s and the all-or-nothing suppression +//! rule for the collision classes (lossy fold, mixed-convention shadow, +//! strip-empty, strip-digit, keyword, redundant). + +use super::*; + +fn idiomatic_config() -> CodeGenConfig { + CodeGenConfig { + idiomatic_enum_aliases: true, + ..Default::default() + } +} + +fn enum_file(file: &str, name: &str, values: Vec) -> FileDescriptorProto { + let mut f = proto3_file(file); + f.enum_type.push(EnumDescriptorProto { + name: Some(name.to_string()), + value: values, + ..Default::default() + }); + f +} + +#[test] +fn aliases_on_by_default() { + let file = enum_file( + "s.proto", + "RuleLevel", + vec![ + enum_value("RULE_LEVEL_UNKNOWN", 0), + enum_value("RULE_LEVEL_HIGH", 1), + ], + ); + let files = generate(&[file], &["s.proto".to_string()], &CodeGenConfig::default()).unwrap(); + let c = joined(&files); + // SHOUTY variant remains, and the idiomatic alias is emitted by default. + assert!(c.contains("RULE_LEVEL_HIGH = 1"), "{c}"); + assert!( + c.contains("const High: Self = Self::RULE_LEVEL_HIGH"), + "{c}" + ); +} + +#[test] +fn aliases_can_be_disabled() { + let file = enum_file( + "s.proto", + "RuleLevel", + vec![ + enum_value("RULE_LEVEL_UNKNOWN", 0), + enum_value("RULE_LEVEL_HIGH", 1), + ], + ); + let config = CodeGenConfig { + idiomatic_enum_aliases: false, + ..Default::default() + }; + let files = generate(&[file], &["s.proto".to_string()], &config).unwrap(); + let c = joined(&files); + assert!(c.contains("RULE_LEVEL_HIGH = 1"), "{c}"); + assert!( + !c.contains("const High"), + "no idiomatic alias when disabled: {c}" + ); +} + +#[test] +fn aliases_strip_prefix_and_camel_case() { + let file = enum_file( + "s.proto", + "RuleLevel", + vec![ + enum_value("RULE_LEVEL_UNKNOWN", 0), + enum_value("RULE_LEVEL_HIGH", 1), + enum_value("RULE_LEVEL_CRITICAL", 2), + ], + ); + let (files, warnings) = + generate_with_diagnostics(&[file], &["s.proto".to_string()], &idiomatic_config()).unwrap(); + let c = joined(&files); + // The SHOUTY_SNAKE_CASE names remain the definitive variants. + assert!(c.contains("RULE_LEVEL_HIGH = 1"), "{c}"); + // Idiomatic aliases are emitted as consts pointing at those variants. + assert!( + c.contains("const Unknown: Self = Self::RULE_LEVEL_UNKNOWN"), + "{c}" + ); + assert!( + c.contains("const High: Self = Self::RULE_LEVEL_HIGH"), + "{c}" + ); + assert!( + c.contains("const Critical: Self = Self::RULE_LEVEL_CRITICAL"), + "{c}" + ); + assert!(warnings.is_empty(), "{warnings:?}"); +} + +#[test] +fn aliases_without_matching_prefix_use_full_name() { + let file = enum_file( + "c.proto", + "Color", + vec![enum_value("RED", 0), enum_value("DARK_GREEN", 1)], + ); + let files = generate(&[file], &["c.proto".to_string()], &idiomatic_config()).unwrap(); + let c = joined(&files); + // Prefix `COLOR_` matches nothing, so full names are CamelCased. + assert!(c.contains("const Red: Self = Self::RED"), "{c}"); + assert!( + c.contains("const DarkGreen: Self = Self::DARK_GREEN"), + "{c}" + ); +} + +#[test] +fn lossy_collision_suppresses_entire_enum() { + let file = enum_file( + "s.proto", + "Weird", + vec![ + enum_value("FOO_BAR", 0), + enum_value("FOO__BAR", 1), // collapses to the same CamelCase as FOO_BAR + enum_value("BAZ", 2), + ], + ); + let (files, warnings) = + generate_with_diagnostics(&[file], &["s.proto".to_string()], &idiomatic_config()).unwrap(); + let c = joined(&files); + // All-or-nothing: even the clean BAZ value gets no alias. + assert!( + !c.contains("const Baz"), + "clean value must be suppressed too: {c}" + ); + assert!(!c.contains("const FooBar"), "{c}"); + // The enum carries a doc note explaining the suppression. + assert!( + c.contains("Idiomatic CamelCase aliases are not generated for this enum"), + "{c}" + ); + // A single build warning names the exact clash. + assert_eq!(warnings.len(), 1, "{warnings:?}"); + let w = warnings[0].to_string(); + assert!( + w.contains("Weird") + && w.contains("FOO_BAR") + && w.contains("FOO__BAR") + && w.contains("FooBar"), + "{w}" + ); + // The structured form exposes the conflict for programmatic handling. + match &warnings[0] { + CodeGenWarning::IdiomaticAliasesSuppressed { + enum_name, + conflicts, + invalid, + .. + } => { + assert_eq!(enum_name, "Weird"); + assert!(invalid.is_empty(), "{invalid:?}"); + assert!( + conflicts.iter().any(|c| c.camel_target == "FooBar" + && c.proto_values.iter().any(|n| n == "FOO_BAR") + && c.proto_values.iter().any(|n| n == "FOO__BAR")), + "{conflicts:?}" + ); + } + } +} + +#[test] +fn mixed_convention_silent_shadow_is_detected() { + // `FOO_BAR`'s CamelCase form (`FooBar`) collides with the literal `FooBar` + // variant — a clash the Rust compiler would accept silently. + let file = enum_file( + "s.proto", + "Mix", + vec![enum_value("FOO_BAR", 0), enum_value("FooBar", 1)], + ); + let (files, warnings) = + generate_with_diagnostics(&[file], &["s.proto".to_string()], &idiomatic_config()).unwrap(); + let c = joined(&files); + assert!( + c.contains("Use the `SHOUTY_SNAKE_CASE` variants directly"), + "{c}" + ); + assert_eq!(warnings.len(), 1, "{warnings:?}"); + let w = warnings[0].to_string(); + assert!(w.contains("FOO_BAR") && w.contains("FooBar"), "{w}"); +} + +#[test] +fn strip_leading_digit_falls_back_to_unstripped() { + let file = enum_file( + "v.proto", + "Version", + vec![ + enum_value("VERSION_UNKNOWN", 0), + enum_value("VERSION_2", 1), // strips to "2", an invalid identifier + enum_value("VERSION_3", 2), + ], + ); + let (files, warnings) = + generate_with_diagnostics(&[file], &["v.proto".to_string()], &idiomatic_config()).unwrap(); + let c = joined(&files); + // The whole enum keeps full (unstripped) names so every alias stays valid. + assert!( + c.contains("const VersionUnknown: Self = Self::VERSION_UNKNOWN"), + "{c}" + ); + assert!(c.contains("const Version2: Self = Self::VERSION_2"), "{c}"); + assert!(c.contains("const Version3: Self = Self::VERSION_3"), "{c}"); + assert!( + warnings.is_empty(), + "fallback should not warn: {warnings:?}" + ); +} + +#[test] +fn strip_empty_remainder_falls_back_to_unstripped() { + // `FOO_` equals the prefix, stripping to "" — fall back to unstripped names. + let file = enum_file( + "f.proto", + "Foo", + vec![enum_value("FOO_UNSET", 0), enum_value("FOO_", 1)], + ); + let (files, warnings) = + generate_with_diagnostics(&[file], &["f.proto".to_string()], &idiomatic_config()).unwrap(); + let c = joined(&files); + assert!(c.contains("const FooUnset: Self = Self::FOO_UNSET"), "{c}"); + assert!(c.contains("const Foo: Self = Self::FOO_"), "{c}"); + assert!(warnings.is_empty(), "{warnings:?}"); +} + +#[test] +fn keyword_alias_is_escaped() { + let file = enum_file( + "k.proto", + "Kind", + vec![enum_value("KIND_UNKNOWN", 0), enum_value("KIND_SELF", 1)], + ); + let files = generate(&[file], &["k.proto".to_string()], &idiomatic_config()).unwrap(); + let c = joined(&files); + // `SELF` folds to the keyword `Self`, escaped to `Self_`. + assert!(c.contains("const Self_: Self = Self::KIND_SELF"), "{c}"); +} + +#[test] +fn strip_decided_for_whole_enum_not_per_value() { + // One value carries the enum prefix, one does not. Because stripping is an + // all-or-nothing per-enum decision, neither is stripped — the result is not + // a mix of `Bar` and `OtherValue`. + let file = enum_file( + "m.proto", + "Mode", + vec![enum_value("MODE_BAR", 0), enum_value("OTHER_VALUE", 1)], + ); + let files = generate(&[file], &["m.proto".to_string()], &idiomatic_config()).unwrap(); + let c = joined(&files); + assert!(c.contains("const ModeBar: Self = Self::MODE_BAR"), "{c}"); + assert!( + c.contains("const OtherValue: Self = Self::OTHER_VALUE"), + "{c}" + ); + assert!(!c.contains("const Bar"), "prefix must not be stripped: {c}"); +} + +#[test] +fn already_camel_with_acronym_is_skipped_not_duplicated() { + // `MyValue` round-trips through the converter, so it is recognized as + // redundant rather than emitted as a mangled `Myvalue` alias. + let file = enum_file( + "s.proto", + "Shape", + vec![enum_value("MyValue", 0), enum_value("OtherValue", 1)], + ); + let files = generate(&[file], &["s.proto".to_string()], &idiomatic_config()).unwrap(); + let c = joined(&files); + assert!(!c.contains("Myvalue"), "must not emit a mangled alias: {c}"); + assert!(!c.contains("impl Shape {"), "all aliases redundant: {c}"); +} + +#[test] +fn alias_const_doc_links_variant_not_duplicates_proto_doc() { + let file = enum_file( + "s.proto", + "RuleLevel", + vec![ + enum_value("RULE_LEVEL_UNKNOWN", 0), + enum_value("RULE_LEVEL_HIGH", 1), + ], + ); + let files = generate(&[file], &["s.proto".to_string()], &idiomatic_config()).unwrap(); + let c = joined(&files); + assert!( + c.contains("Idiomatic alias for [`Self::RULE_LEVEL_HIGH`]"), + "{c}" + ); +} + +#[test] +fn redundant_alias_is_skipped() { + // Values already in CamelCase need no alias and trigger no suppression. + let file = enum_file( + "s.proto", + "State", + vec![enum_value("Active", 0), enum_value("Inactive", 1)], + ); + let (files, warnings) = + generate_with_diagnostics(&[file], &["s.proto".to_string()], &idiomatic_config()).unwrap(); + let c = joined(&files); + assert!( + !c.contains("impl State {"), + "no alias impl block expected: {c}" + ); + assert!(warnings.is_empty(), "{warnings:?}"); +} + +#[test] +fn allow_alias_values_get_idiomatic_aliases() { + let mut file = proto3_file("code.proto"); + file.enum_type.push(EnumDescriptorProto { + name: Some("Code".to_string()), + value: vec![ + enum_value("CODE_OK", 0), + enum_value("CODE_SUCCESS", 0), // proto allow_alias of CODE_OK + enum_value("CODE_ERROR", 1), + ], + options: (crate::generated::descriptor::EnumOptions { + allow_alias: Some(true), + ..Default::default() + }) + .into(), + ..Default::default() + }); + let files = generate(&[file], &["code.proto".to_string()], &idiomatic_config()).unwrap(); + let c = joined(&files); + // Primary and its proto alias both get an idiomatic const, pointing at the + // canonical variant for the number. + assert!(c.contains("const Ok: Self = Self::CODE_OK"), "{c}"); + assert!(c.contains("const Success: Self = Self::CODE_OK"), "{c}"); + assert!(c.contains("const Error: Self = Self::CODE_ERROR"), "{c}"); +} diff --git a/buffa-codegen/src/tests/mod.rs b/buffa-codegen/src/tests/mod.rs index 626390e..67a8374 100644 --- a/buffa-codegen/src/tests/mod.rs +++ b/buffa-codegen/src/tests/mod.rs @@ -53,6 +53,7 @@ mod comments; mod custom_attributes; mod feature_gating; mod generation; +mod idiomatic_enums; mod json_codegen; mod naming; mod proto2; diff --git a/buffa-descriptor/src/generated/google.protobuf.compiler.plugin.rs b/buffa-descriptor/src/generated/google.protobuf.compiler.plugin.rs index d870704..1aae934 100644 --- a/buffa-descriptor/src/generated/google.protobuf.compiler.plugin.rs +++ b/buffa-descriptor/src/generated/google.protobuf.compiler.plugin.rs @@ -1199,6 +1199,17 @@ pub mod code_generator_response { FEATURE_PROTO3_OPTIONAL = 1i32, FEATURE_SUPPORTS_EDITIONS = 2i32, } + impl Feature { + ///Idiomatic alias for [`Self::FEATURE_NONE`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const None: Self = Self::FEATURE_NONE; + ///Idiomatic alias for [`Self::FEATURE_PROTO3_OPTIONAL`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Proto3Optional: Self = Self::FEATURE_PROTO3_OPTIONAL; + ///Idiomatic alias for [`Self::FEATURE_SUPPORTS_EDITIONS`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const SupportsEditions: Self = Self::FEATURE_SUPPORTS_EDITIONS; + } impl ::core::default::Default for Feature { fn default() -> Self { Self::FEATURE_NONE diff --git a/buffa-descriptor/src/generated/google.protobuf.descriptor.rs b/buffa-descriptor/src/generated/google.protobuf.descriptor.rs index 71a2154..2022e47 100644 --- a/buffa-descriptor/src/generated/google.protobuf.descriptor.rs +++ b/buffa-descriptor/src/generated/google.protobuf.descriptor.rs @@ -36,6 +36,47 @@ pub enum Edition { /// support a new edition. EDITION_MAX = 2147483647i32, } +impl Edition { + ///Idiomatic alias for [`Self::EDITION_UNKNOWN`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const EditionUnknown: Self = Self::EDITION_UNKNOWN; + ///Idiomatic alias for [`Self::EDITION_LEGACY`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const EditionLegacy: Self = Self::EDITION_LEGACY; + ///Idiomatic alias for [`Self::EDITION_PROTO2`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const EditionProto2: Self = Self::EDITION_PROTO2; + ///Idiomatic alias for [`Self::EDITION_PROTO3`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const EditionProto3: Self = Self::EDITION_PROTO3; + ///Idiomatic alias for [`Self::EDITION_2023`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Edition2023: Self = Self::EDITION_2023; + ///Idiomatic alias for [`Self::EDITION_2024`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Edition2024: Self = Self::EDITION_2024; + ///Idiomatic alias for [`Self::EDITION_UNSTABLE`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const EditionUnstable: Self = Self::EDITION_UNSTABLE; + ///Idiomatic alias for [`Self::EDITION_1_TEST_ONLY`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Edition1TestOnly: Self = Self::EDITION_1_TEST_ONLY; + ///Idiomatic alias for [`Self::EDITION_2_TEST_ONLY`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Edition2TestOnly: Self = Self::EDITION_2_TEST_ONLY; + ///Idiomatic alias for [`Self::EDITION_99997_TEST_ONLY`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Edition99997TestOnly: Self = Self::EDITION_99997_TEST_ONLY; + ///Idiomatic alias for [`Self::EDITION_99998_TEST_ONLY`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Edition99998TestOnly: Self = Self::EDITION_99998_TEST_ONLY; + ///Idiomatic alias for [`Self::EDITION_99999_TEST_ONLY`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Edition99999TestOnly: Self = Self::EDITION_99999_TEST_ONLY; + ///Idiomatic alias for [`Self::EDITION_MAX`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const EditionMax: Self = Self::EDITION_MAX; +} impl ::core::default::Default for Edition { fn default() -> Self { Self::EDITION_UNKNOWN @@ -228,6 +269,17 @@ pub enum SymbolVisibility { VISIBILITY_LOCAL = 1i32, VISIBILITY_EXPORT = 2i32, } +impl SymbolVisibility { + ///Idiomatic alias for [`Self::VISIBILITY_UNSET`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const VisibilityUnset: Self = Self::VISIBILITY_UNSET; + ///Idiomatic alias for [`Self::VISIBILITY_LOCAL`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const VisibilityLocal: Self = Self::VISIBILITY_LOCAL; + ///Idiomatic alias for [`Self::VISIBILITY_EXPORT`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const VisibilityExport: Self = Self::VISIBILITY_EXPORT; +} impl ::core::default::Default for SymbolVisibility { fn default() -> Self { Self::VISIBILITY_UNSET @@ -3478,6 +3530,14 @@ pub mod extension_range_options { DECLARATION = 0i32, UNVERIFIED = 1i32, } + impl VerificationState { + ///Idiomatic alias for [`Self::DECLARATION`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Declaration: Self = Self::DECLARATION; + ///Idiomatic alias for [`Self::UNVERIFIED`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Unverified: Self = Self::UNVERIFIED; + } impl ::core::default::Default for VerificationState { fn default() -> Self { Self::DECLARATION @@ -4830,6 +4890,62 @@ pub mod field_descriptor_proto { /// Uses ZigZag encoding. TYPE_SINT64 = 18i32, } + impl Type { + ///Idiomatic alias for [`Self::TYPE_DOUBLE`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Double: Self = Self::TYPE_DOUBLE; + ///Idiomatic alias for [`Self::TYPE_FLOAT`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Float: Self = Self::TYPE_FLOAT; + ///Idiomatic alias for [`Self::TYPE_INT64`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Int64: Self = Self::TYPE_INT64; + ///Idiomatic alias for [`Self::TYPE_UINT64`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Uint64: Self = Self::TYPE_UINT64; + ///Idiomatic alias for [`Self::TYPE_INT32`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Int32: Self = Self::TYPE_INT32; + ///Idiomatic alias for [`Self::TYPE_FIXED64`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Fixed64: Self = Self::TYPE_FIXED64; + ///Idiomatic alias for [`Self::TYPE_FIXED32`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Fixed32: Self = Self::TYPE_FIXED32; + ///Idiomatic alias for [`Self::TYPE_BOOL`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Bool: Self = Self::TYPE_BOOL; + ///Idiomatic alias for [`Self::TYPE_STRING`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const String: Self = Self::TYPE_STRING; + ///Idiomatic alias for [`Self::TYPE_GROUP`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Group: Self = Self::TYPE_GROUP; + ///Idiomatic alias for [`Self::TYPE_MESSAGE`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Message: Self = Self::TYPE_MESSAGE; + ///Idiomatic alias for [`Self::TYPE_BYTES`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Bytes: Self = Self::TYPE_BYTES; + ///Idiomatic alias for [`Self::TYPE_UINT32`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Uint32: Self = Self::TYPE_UINT32; + ///Idiomatic alias for [`Self::TYPE_ENUM`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Enum: Self = Self::TYPE_ENUM; + ///Idiomatic alias for [`Self::TYPE_SFIXED32`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Sfixed32: Self = Self::TYPE_SFIXED32; + ///Idiomatic alias for [`Self::TYPE_SFIXED64`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Sfixed64: Self = Self::TYPE_SFIXED64; + ///Idiomatic alias for [`Self::TYPE_SINT32`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Sint32: Self = Self::TYPE_SINT32; + ///Idiomatic alias for [`Self::TYPE_SINT64`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Sint64: Self = Self::TYPE_SINT64; + } impl ::core::default::Default for Type { fn default() -> Self { Self::TYPE_DOUBLE @@ -5033,6 +5149,17 @@ pub mod field_descriptor_proto { /// can be used to get this behavior. LABEL_REQUIRED = 2i32, } + impl Label { + ///Idiomatic alias for [`Self::LABEL_OPTIONAL`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Optional: Self = Self::LABEL_OPTIONAL; + ///Idiomatic alias for [`Self::LABEL_REPEATED`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Repeated: Self = Self::LABEL_REPEATED; + ///Idiomatic alias for [`Self::LABEL_REQUIRED`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Required: Self = Self::LABEL_REQUIRED; + } impl ::core::default::Default for Label { fn default() -> Self { Self::LABEL_OPTIONAL @@ -8965,6 +9092,17 @@ pub mod file_options { /// Generate code using MessageLite and the lite runtime. LITE_RUNTIME = 3i32, } + impl OptimizeMode { + ///Idiomatic alias for [`Self::SPEED`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Speed: Self = Self::SPEED; + ///Idiomatic alias for [`Self::CODE_SIZE`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const CodeSize: Self = Self::CODE_SIZE; + ///Idiomatic alias for [`Self::LITE_RUNTIME`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const LiteRuntime: Self = Self::LITE_RUNTIME; + } impl ::core::default::Default for OptimizeMode { fn default() -> Self { Self::SPEED @@ -11181,6 +11319,17 @@ pub mod field_options { CORD = 1i32, STRING_PIECE = 2i32, } + impl CType { + ///Idiomatic alias for [`Self::STRING`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const String: Self = Self::STRING; + ///Idiomatic alias for [`Self::CORD`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Cord: Self = Self::CORD; + ///Idiomatic alias for [`Self::STRING_PIECE`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const StringPiece: Self = Self::STRING_PIECE; + } impl ::core::default::Default for CType { fn default() -> Self { Self::STRING @@ -11319,6 +11468,17 @@ pub mod field_options { /// Use JavaScript numbers. JS_NUMBER = 2i32, } + impl JSType { + ///Idiomatic alias for [`Self::JS_NORMAL`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const JsNormal: Self = Self::JS_NORMAL; + ///Idiomatic alias for [`Self::JS_STRING`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const JsString: Self = Self::JS_STRING; + ///Idiomatic alias for [`Self::JS_NUMBER`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const JsNumber: Self = Self::JS_NUMBER; + } impl ::core::default::Default for JSType { fn default() -> Self { Self::JS_NORMAL @@ -11457,6 +11617,17 @@ pub mod field_options { RETENTION_RUNTIME = 1i32, RETENTION_SOURCE = 2i32, } + impl OptionRetention { + ///Idiomatic alias for [`Self::RETENTION_UNKNOWN`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const RetentionUnknown: Self = Self::RETENTION_UNKNOWN; + ///Idiomatic alias for [`Self::RETENTION_RUNTIME`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const RetentionRuntime: Self = Self::RETENTION_RUNTIME; + ///Idiomatic alias for [`Self::RETENTION_SOURCE`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const RetentionSource: Self = Self::RETENTION_SOURCE; + } impl ::core::default::Default for OptionRetention { fn default() -> Self { Self::RETENTION_UNKNOWN @@ -11611,6 +11782,38 @@ pub mod field_options { TARGET_TYPE_SERVICE = 8i32, TARGET_TYPE_METHOD = 9i32, } + impl OptionTargetType { + ///Idiomatic alias for [`Self::TARGET_TYPE_UNKNOWN`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const TargetTypeUnknown: Self = Self::TARGET_TYPE_UNKNOWN; + ///Idiomatic alias for [`Self::TARGET_TYPE_FILE`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const TargetTypeFile: Self = Self::TARGET_TYPE_FILE; + ///Idiomatic alias for [`Self::TARGET_TYPE_EXTENSION_RANGE`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const TargetTypeExtensionRange: Self = Self::TARGET_TYPE_EXTENSION_RANGE; + ///Idiomatic alias for [`Self::TARGET_TYPE_MESSAGE`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const TargetTypeMessage: Self = Self::TARGET_TYPE_MESSAGE; + ///Idiomatic alias for [`Self::TARGET_TYPE_FIELD`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const TargetTypeField: Self = Self::TARGET_TYPE_FIELD; + ///Idiomatic alias for [`Self::TARGET_TYPE_ONEOF`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const TargetTypeOneof: Self = Self::TARGET_TYPE_ONEOF; + ///Idiomatic alias for [`Self::TARGET_TYPE_ENUM`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const TargetTypeEnum: Self = Self::TARGET_TYPE_ENUM; + ///Idiomatic alias for [`Self::TARGET_TYPE_ENUM_ENTRY`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const TargetTypeEnumEntry: Self = Self::TARGET_TYPE_ENUM_ENTRY; + ///Idiomatic alias for [`Self::TARGET_TYPE_SERVICE`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const TargetTypeService: Self = Self::TARGET_TYPE_SERVICE; + ///Idiomatic alias for [`Self::TARGET_TYPE_METHOD`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const TargetTypeMethod: Self = Self::TARGET_TYPE_METHOD; + } impl ::core::default::Default for OptionTargetType { fn default() -> Self { Self::TARGET_TYPE_UNKNOWN @@ -15091,6 +15294,17 @@ pub mod method_options { /// idempotent, but may have side effects IDEMPOTENT = 2i32, } + impl IdempotencyLevel { + ///Idiomatic alias for [`Self::IDEMPOTENCY_UNKNOWN`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const IdempotencyUnknown: Self = Self::IDEMPOTENCY_UNKNOWN; + ///Idiomatic alias for [`Self::NO_SIDE_EFFECTS`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const NoSideEffects: Self = Self::NO_SIDE_EFFECTS; + ///Idiomatic alias for [`Self::IDEMPOTENT`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Idempotent: Self = Self::IDEMPOTENT; + } impl ::core::default::Default for IdempotencyLevel { fn default() -> Self { Self::IDEMPOTENCY_UNKNOWN @@ -16947,6 +17161,20 @@ pub mod feature_set { IMPLICIT = 2i32, LEGACY_REQUIRED = 3i32, } + impl FieldPresence { + ///Idiomatic alias for [`Self::FIELD_PRESENCE_UNKNOWN`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const FieldPresenceUnknown: Self = Self::FIELD_PRESENCE_UNKNOWN; + ///Idiomatic alias for [`Self::EXPLICIT`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Explicit: Self = Self::EXPLICIT; + ///Idiomatic alias for [`Self::IMPLICIT`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Implicit: Self = Self::IMPLICIT; + ///Idiomatic alias for [`Self::LEGACY_REQUIRED`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const LegacyRequired: Self = Self::LEGACY_REQUIRED; + } impl ::core::default::Default for FieldPresence { fn default() -> Self { Self::FIELD_PRESENCE_UNKNOWN @@ -17094,6 +17322,17 @@ pub mod feature_set { OPEN = 1i32, CLOSED = 2i32, } + impl EnumType { + ///Idiomatic alias for [`Self::ENUM_TYPE_UNKNOWN`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const EnumTypeUnknown: Self = Self::ENUM_TYPE_UNKNOWN; + ///Idiomatic alias for [`Self::OPEN`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Open: Self = Self::OPEN; + ///Idiomatic alias for [`Self::CLOSED`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Closed: Self = Self::CLOSED; + } impl ::core::default::Default for EnumType { fn default() -> Self { Self::ENUM_TYPE_UNKNOWN @@ -17233,6 +17472,17 @@ pub mod feature_set { PACKED = 1i32, EXPANDED = 2i32, } + impl RepeatedFieldEncoding { + ///Idiomatic alias for [`Self::REPEATED_FIELD_ENCODING_UNKNOWN`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const RepeatedFieldEncodingUnknown: Self = Self::REPEATED_FIELD_ENCODING_UNKNOWN; + ///Idiomatic alias for [`Self::PACKED`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Packed: Self = Self::PACKED; + ///Idiomatic alias for [`Self::EXPANDED`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Expanded: Self = Self::EXPANDED; + } impl ::core::default::Default for RepeatedFieldEncoding { fn default() -> Self { Self::REPEATED_FIELD_ENCODING_UNKNOWN @@ -17379,6 +17629,17 @@ pub mod feature_set { VERIFY = 2i32, NONE = 3i32, } + impl Utf8Validation { + ///Idiomatic alias for [`Self::UTF8_VALIDATION_UNKNOWN`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Utf8ValidationUnknown: Self = Self::UTF8_VALIDATION_UNKNOWN; + ///Idiomatic alias for [`Self::VERIFY`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Verify: Self = Self::VERIFY; + ///Idiomatic alias for [`Self::NONE`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const None: Self = Self::NONE; + } impl ::core::default::Default for Utf8Validation { fn default() -> Self { Self::UTF8_VALIDATION_UNKNOWN @@ -17519,6 +17780,17 @@ pub mod feature_set { LENGTH_PREFIXED = 1i32, DELIMITED = 2i32, } + impl MessageEncoding { + ///Idiomatic alias for [`Self::MESSAGE_ENCODING_UNKNOWN`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const MessageEncodingUnknown: Self = Self::MESSAGE_ENCODING_UNKNOWN; + ///Idiomatic alias for [`Self::LENGTH_PREFIXED`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const LengthPrefixed: Self = Self::LENGTH_PREFIXED; + ///Idiomatic alias for [`Self::DELIMITED`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Delimited: Self = Self::DELIMITED; + } impl ::core::default::Default for MessageEncoding { fn default() -> Self { Self::MESSAGE_ENCODING_UNKNOWN @@ -17659,6 +17931,17 @@ pub mod feature_set { ALLOW = 1i32, LEGACY_BEST_EFFORT = 2i32, } + impl JsonFormat { + ///Idiomatic alias for [`Self::JSON_FORMAT_UNKNOWN`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const JsonFormatUnknown: Self = Self::JSON_FORMAT_UNKNOWN; + ///Idiomatic alias for [`Self::ALLOW`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Allow: Self = Self::ALLOW; + ///Idiomatic alias for [`Self::LEGACY_BEST_EFFORT`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const LegacyBestEffort: Self = Self::LEGACY_BEST_EFFORT; + } impl ::core::default::Default for JsonFormat { fn default() -> Self { Self::JSON_FORMAT_UNKNOWN @@ -17800,6 +18083,17 @@ pub mod feature_set { STYLE2024 = 1i32, STYLE_LEGACY = 2i32, } + impl EnforceNamingStyle { + ///Idiomatic alias for [`Self::ENFORCE_NAMING_STYLE_UNKNOWN`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const EnforceNamingStyleUnknown: Self = Self::ENFORCE_NAMING_STYLE_UNKNOWN; + ///Idiomatic alias for [`Self::STYLE2024`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Style2024: Self = Self::STYLE2024; + ///Idiomatic alias for [`Self::STYLE_LEGACY`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const StyleLegacy: Self = Self::STYLE_LEGACY; + } impl ::core::default::Default for EnforceNamingStyle { fn default() -> Self { Self::ENFORCE_NAMING_STYLE_UNKNOWN @@ -18092,6 +18386,23 @@ pub mod feature_set { /// This is the recommended setting for new protos. STRICT = 4i32, } + impl DefaultSymbolVisibility { + ///Idiomatic alias for [`Self::DEFAULT_SYMBOL_VISIBILITY_UNKNOWN`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const DefaultSymbolVisibilityUnknown: Self = Self::DEFAULT_SYMBOL_VISIBILITY_UNKNOWN; + ///Idiomatic alias for [`Self::EXPORT_ALL`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const ExportAll: Self = Self::EXPORT_ALL; + ///Idiomatic alias for [`Self::EXPORT_TOP_LEVEL`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const ExportTopLevel: Self = Self::EXPORT_TOP_LEVEL; + ///Idiomatic alias for [`Self::LOCAL_ALL`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const LocalAll: Self = Self::LOCAL_ALL; + ///Idiomatic alias for [`Self::STRICT`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Strict: Self = Self::STRICT; + } impl ::core::default::Default for DefaultSymbolVisibility { fn default() -> Self { Self::DEFAULT_SYMBOL_VISIBILITY_UNKNOWN @@ -20499,6 +20810,17 @@ pub mod generated_code_info { /// An alias to the element is returned. ALIAS = 2i32, } + impl Semantic { + ///Idiomatic alias for [`Self::NONE`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const None: Self = Self::NONE; + ///Idiomatic alias for [`Self::SET`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Set: Self = Self::SET; + ///Idiomatic alias for [`Self::ALIAS`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Alias: Self = Self::ALIAS; + } impl ::core::default::Default for Semantic { fn default() -> Self { Self::NONE diff --git a/buffa-types/src/generated/google.protobuf.struct.rs b/buffa-types/src/generated/google.protobuf.struct.rs index d35f48a..d6ef4a9 100644 --- a/buffa-types/src/generated/google.protobuf.struct.rs +++ b/buffa-types/src/generated/google.protobuf.struct.rs @@ -12,6 +12,11 @@ pub enum NullValue { /// Null value. NULL_VALUE = 0i32, } +impl NullValue { + ///Idiomatic alias for [`Self::NULL_VALUE`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const NullValue: Self = Self::NULL_VALUE; +} impl ::core::default::Default for NullValue { fn default() -> Self { Self::NULL_VALUE diff --git a/examples/logging/src/gen/log.v1.log.rs b/examples/logging/src/gen/log.v1.log.rs index 40b42ff..0521f2d 100644 --- a/examples/logging/src/gen/log.v1.log.rs +++ b/examples/logging/src/gen/log.v1.log.rs @@ -12,6 +12,26 @@ pub enum Severity { ERROR = 4i32, FATAL = 5i32, } +impl Severity { + ///Idiomatic alias for [`Self::SEVERITY_UNSPECIFIED`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const SeverityUnspecified: Self = Self::SEVERITY_UNSPECIFIED; + ///Idiomatic alias for [`Self::DEBUG`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Debug: Self = Self::DEBUG; + ///Idiomatic alias for [`Self::INFO`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Info: Self = Self::INFO; + ///Idiomatic alias for [`Self::WARN`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Warn: Self = Self::WARN; + ///Idiomatic alias for [`Self::ERROR`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Error: Self = Self::ERROR; + ///Idiomatic alias for [`Self::FATAL`]; `Debug` prints the variant name. + #[allow(non_upper_case_globals)] + pub const Fatal: Self = Self::FATAL; +} impl ::core::default::Default for Severity { fn default() -> Self { Self::SEVERITY_UNSPECIFIED