Skip to content

codegen: re-export __buffa ancillary types at natural paths#96

Merged
iainmcgin merged 2 commits intomainfrom
codegen/natural-reexports
May 5, 2026
Merged

codegen: re-export __buffa ancillary types at natural paths#96
iainmcgin merged 2 commits intomainfrom
codegen/natural-reexports

Conversation

@iainmcgin
Copy link
Copy Markdown
Collaborator

Summary

Closes #80.

Ancillary generated types (views, oneof enums, view-of-oneof enums, file-level extension consts, register_types) live unconditionally under the per-package __buffa:: sentinel module so they can never collide with user-declared proto types. As an ergonomic convenience, codegen now additionally emits a pub use re-export for each one at the path a Rust user would write first — mirroring the pre-0.4.0 (and prost) layout:

ancillary item canonical (__buffa::) natural re-export
view of Foo __buffa::view::FooView FooView
view of Foo.Bar __buffa::view::foo::BarView foo::BarView
oneof kind of Foo __buffa::oneof::foo::Kind foo::Kind
view of oneof kind __buffa::view::oneof::foo::Kind foo::KindView (renamed via as)
file-level extend __buffa::ext::MY_EXT MY_EXT
register_types __buffa::register_types register_types

A re-export is silently skipped when the natural name is already taken by a real proto item (message, enum, extension const) or by another candidate re-export. When two candidates collide with each other, both are dropped — never "first one wins" — so the result is order-independent. The canonical __buffa:: path is the source of truth: generated method signatures, field types, and downstream codegen always use it, so a skipped re-export never breaks anything; only a hand-written import of the natural path needs adjusting.

This is the "simple with fallback" approach agreed in #80. No new config flag — the re-exports are unconditional and additive.

Highlights

  • self::__buffa::… prefix on package-root re-exports. Consumers that nest packages with use super::* chains (e.g. buffa-build's _include.rs for google.api + google.api.expr.v1alpha1) glob-import a parent package's __buffa into the child's scope. Rust's import-resolution pass then treats a bare __buffa in a use path as ambiguous against the locally-include!d one (E0659 — glob-import vs. macro-expanded item). self:: disambiguates. Caught by the googleapis stress test; locked in by a new buffa-test/protos/nestpkg_{outer,inner} fixture that runs in CI.
  • #[doc(inline)] on every re-export so cargo doc renders the full type page at the natural path instead of a "Re-export of …" stub.
  • examples/conflicts is a new self-contained example whose proto deliberately shadows every re-export shape (Probe + nested Reading/ReadingView + top-level ProbeView). It demonstrates the __buffa:: import-alias fallback for the cases where a natural re-export was dropped, alongside Event showing the happy path.
  • examples/addressbook updated to use the natural person::Address path (was __buffa::oneof::person::Address), with a comment pointing at examples/conflicts for the conflict story.

Generated-output diff

The WKT regen shows the shape (buffa-types/src/generated/google.protobuf.mod.rs):

#[doc(inline)]
pub use self::__buffa::view::AnyView;
#[doc(inline)]
pub use self::__buffa::view::DurationView;
// … one line per top-level message

and Value's oneof in google.protobuf.struct.rs:

pub mod value {
    #[allow(unused_imports)]
    use super::*;
    #[doc(inline)]
    pub use super::__buffa::oneof::value::Kind;
    #[doc(inline)]
    pub use super::__buffa::view::oneof::value::Kind as KindView;
}

Notes / trade-offs

  • Adding a proto type can rebind a natural path. message FooView next to message Foo makes pkg::FooView resolve to the new struct instead of Foo's view re-export. The canonical __buffa:: path is always stable. Documented in DESIGN.md and CHANGELOG.md — this is the agreed trade-off (predictability over stability of every spelling).
  • __buffa:: stays visible in docs (no #[doc(hidden)]), per the discussion in New deeply nested oneof and view paths are difficult to use #80 — predictable behavior over a flickering doc surface.
  • Messages with only a oneof now produce a pub mod {msg_snake} { … } block in the owned tree (to host the re-export); pre-New deeply nested oneof and view paths are difficult to use #80 they did not. Documented in DESIGN.md.

Testing

  • cargo test --workspace — 1525 pass (11 new in buffa-codegen/src/tests/reexports.rs covering happy path at all nesting depths, every collision rule, views-disabled path, and the pub mod block being emitted for re-exports-only messages; 2 new in buffa-test/src/tests/nestpkg.rs for the nested-package consumer pattern)
  • cargo clippy --workspace --all-targets -- -D warnings — clean
  • cargo build --workspace --no-default-features — no_std clean
  • task build-examples — all 4 build (incl. new conflicts); cargo run on conflicts passes its assertions
  • task stress-googleapis — PASS (this is what caught the self:: requirement)
  • task lint-md — clean
  • task gen-wkt-types / task gen-bootstrap-types — checked-in generated code regenerated
  • conformance binary builds; full suite runs in CI (local GHCR pull blocked by missing docker-credential-gh)

Ancillary generated types (views, oneof enums, view-of-oneof enums,
file-level extension consts, register_types) live unconditionally under
the per-package __buffa:: sentinel module so they can never collide with
user-declared proto types. As an ergonomic convenience, additionally
emit a pub use re-export for each one at the path a user would write
first, mirroring the pre-0.4.0 (and prost) layout:

  pkg::FooView           <- __buffa::view::FooView
  pkg::foo::BarView      <- __buffa::view::foo::BarView
  pkg::foo::Kind         <- __buffa::oneof::foo::Kind
  pkg::foo::KindView     <- __buffa::view::oneof::foo::Kind  (renamed)
  pkg::MY_EXT            <- __buffa::ext::MY_EXT
  pkg::register_types    <- __buffa::register_types

A re-export is silently skipped when the natural name is already taken by
a real proto item or by another candidate re-export. When two candidates
collide with each other, both are dropped — never "first one wins" — so
the result is order-independent. The canonical __buffa:: path is the
source of truth: generated method signatures, field types, and downstream
codegen always use it, so a skipped re-export never breaks anything.

Package-root re-exports use a self::__buffa:: prefix because consumers
that nest packages with use super::* chains (buffa-build's _include.rs
does this) glob-import a parent package's __buffa, and Rust's import
resolution treats a glob-imported name as ambiguous against a
macro-expanded local one (E0659). Caught by the googleapis stress test;
locked in by a new buffa-test/protos/nestpkg_{outer,inner} fixture that
runs in CI.

Re-exports carry #[doc(inline)] so cargo doc renders the full type page
at the natural path instead of a re-export stub.

examples/conflicts demonstrates a proto that deliberately shadows every
re-export shape and the __buffa:: import-alias fallback.

Closes #80
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 5, 2026

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@iainmcgin iainmcgin marked this pull request as ready for review May 5, 2026 19:15
@iainmcgin iainmcgin requested a review from rpb-ant May 5, 2026 19:15
@iainmcgin iainmcgin enabled auto-merge (squash) May 5, 2026 19:15
rpb-ant
rpb-ant previously approved these changes May 5, 2026
@iainmcgin iainmcgin merged commit 4d29dde into main May 5, 2026
7 checks passed
@iainmcgin iainmcgin deleted the codegen/natural-reexports branch May 5, 2026 20:16
@github-actions github-actions Bot locked and limited conversation to collaborators May 5, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

New deeply nested oneof and view paths are difficult to use

2 participants