Skip to content

feat(buffa): add MessageFullName trait#108

Merged
iainmcgin merged 4 commits into
anthropics:mainfrom
yordis:yordis/full-name
May 14, 2026
Merged

feat(buffa): add MessageFullName trait#108
iainmcgin merged 4 commits into
anthropics:mainfrom
yordis:yordis/full-name

Conversation

@yordis
Copy link
Copy Markdown
Contributor

@yordis yordis commented May 9, 2026

  • Event-sourced systems identify each event on the wire by its fully-qualified type name (e.g. user.UserCreated). To use a buffa-generated message directly as a domain event. Skipping the usual hand-written mapping layer between "domain type" and "storage type". Callers need that name as a compile-time constant on the type itself. Today it isn't there: the closest surface, ExtensionSet::PROTO_FQN, is bundled with the extension machinery and only emitted when a message opts into unknown_fields=true, which adds a hidden __buffa_unknown_fields field to the struct and forces every struct-literal construction in domain code to trail ..Default::default(). Reaching for buffa::ExtensionSet just to read the FQN therefore drags in storage and ergonomic costs the caller doesn't want.
  • Following the same shape as protocolbuffers/protobuf#27111, this PR introduces buffa::MessageFullName as a standalone trait with a single const FULL_NAME: &'static str item. Kept out of Message precisely so the FQN concern stays orthogonal and adding it doesn't break any hand-written impl Message in downstream code. Generic call sites import the trait (use buffa::MessageFullName;) and read T::FULL_NAME at compile time with no runtime indirection, so event stores, type registries, and Any type-URL construction can dispatch on it instead of maintaining a hand-written match arm per message.
  • ExtensionSet::PROTO_FQN is intentionally left in place to keep this change additive; the two consts are emitted from the same proto_fqn source in codegen so they cannot drift, and that invariant is documented at both sites.
  • The name MessageFullName / FULL_NAME mirrors the convention used across the protobuf ecosystem (and matches protocolbuffers/protobuf#27111 to keep the two PRs aligned). Not attached to it. Happy to rename if a different spelling reads better here.

Related: TrogonStack/trogonai#108.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 9, 2026

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

@yordis
Copy link
Copy Markdown
Contributor Author

yordis commented May 9, 2026

To be extra clear, intentionally opt-out from unknown_fields=true since that causes extra fields that I wish to avoid to codegen, having the trait to a very specific things helps with the Interface segregation principle, which is effectively what is going on for me

@yordis yordis force-pushed the yordis/full-name branch 2 times, most recently from 0be12cc to ad3c34e Compare May 13, 2026 22:12
@yordis
Copy link
Copy Markdown
Contributor Author

yordis commented May 13, 2026

@iainmcgin could help me on this one? right now I decided to manually maintain the string since the extra extension stuff add a lot of noise in the PR, I am hoping this would be the direction to avoid the interface segregation anti-pattern. Most likely, breaking changing the extension set I think

Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
@yordis yordis force-pushed the yordis/full-name branch from ad3c34e to 601aff4 Compare May 14, 2026 14:32
@iainmcgin
Copy link
Copy Markdown
Collaborator

Hi yes I've been tinkering with this one a bit, thanks for the contribution. It's a very busy week but I'll try and get something together in the next few days for this - likely as part of a 0.6.0 release.

@yordis
Copy link
Copy Markdown
Contributor Author

yordis commented May 14, 2026

Aye, I am around to do the work, just let me know and I jump in

iainmcgin added 2 commits May 14, 2026 21:38
…rait

Reshape the trait before it ships and the surface is semver-frozen:

- `MessageFullName` -> `MessageName`. Matches `prost::Name`, the
  ecosystem precedent for "compile-time access to a message's protobuf
  identifiers", and reads cleaner as a generic bound.

- Add `const PACKAGE`, `NAME`, and `TYPE_URL` alongside `FULL_NAME`.
  `prost::Name` ships `PACKAGE` + `NAME` + runtime `full_name()` /
  `type_url()`; buffa already computes all four at codegen time, so
  ship them as `&'static str` literals -- zero runtime cost. `PACKAGE`
  and `NAME` matter independently because the dotted `FULL_NAME` cannot
  be split unambiguously (`foo.Bar.Baz` could be package `foo.Bar` +
  message `Baz` or package `foo` + nested `Bar.Baz`); codegen knows
  which. Adding required consts to a published trait is a breaking
  change, so land the full set now. The trait's `TYPE_URL` is the same
  string as the inherent `Foo::TYPE_URL` const codegen has emitted since
  0.4.0 and is documented as such.

- Drop the `Message` supertrait and implement the trait for view types
  too (`impl<'a> MessageName for FooView<'a>`). The use case the PR
  targets -- type-erased event registries, structured logging -- wants
  to read the FQN from a zero-copy view without round-tripping through
  `to_owned_message()`, and a name-keyed registry shouldn't have to
  prove it can encode just to register a type. The const doesn't reach
  into the wire codec, so nothing requires `Message`.

Codegen extracts the impl emission into a shared
`message_name_impl(package, fqn, generics, ty)` helper used by both
`impl_message.rs` (owned) and `view.rs` (view). Tests cover the package
/ name split, the nested-message dotted `NAME`, the inherent-const
parity, and the view-type impl. `docs/migration-from-prost.md` now
documents the `prost::Name -> MessageName` migration; `docs/guide.md`'s
hand-written-impl example covers all four consts. Regen'd
`buffa-descriptor`, `buffa-types`, `examples/logging`.
@iainmcgin
Copy link
Copy Markdown
Collaborator

[claude code]

Thanks for the PR — the motivation (compile-time FQN access for type-erased dispatch without descriptor machinery) is a real gap and the trait shape is the right tool.

I pushed two commits to the fork (maintainerCanModify is on, hope that's OK):

  1. ef0d8bc — merge of main. The view-Serialize (view: impl Serialize for generated view types (#83) #106), empty-ancillary-file cleanup (fix(buffa-codegen): omit empty ancillary content files #107), and feature-gated codegen + buffa-descriptor regen (feat(buffa-codegen): gate generated json/views/text impls on crate features #117/feat(buffa-descriptor): regen with views/json/text/arbitrary behind crate features #118) all touch the generated code and the codegen modules, so the rebase was non-trivial.

  2. b0fa38a — API reshape, since the trait surface gets semver-frozen the moment it ships:

    • MessageFullNameMessageName — matches prost::Name, the ecosystem precedent.
    • Add PACKAGE, NAME, TYPE_URL alongside FULL_NAMEprost::Name ships PACKAGE + NAME + runtime full_name()/type_url(); buffa already computes all four at codegen time, so they ship as &'static str literals (zero runtime cost). PACKAGE and NAME matter independently because FULL_NAME can't be split unambiguously (foo.Bar.Baz could be package foo.Bar + message Baz, or package foo + nested Bar.Baz); codegen knows which. Adding required consts to a published trait is a breaking change, so the full set lands now.
    • Drop the Message supertrait, implement for view types too — the use case you describe (event registries, dispatch by FQN) wants T::FULL_NAME to work on a zero-copy FooView<'a> without to_owned_message(), and a name-keyed registry shouldn't have to prove it can encode just to register a type.

    The trait's TYPE_URL is the same string as the inherent Foo::TYPE_URL const codegen has emitted since 0.4.0 — documented and tested. docs/migration-from-prost.md now has the prost::Name → MessageName row.

If any of this doesn't fit your use case, happy to discuss — the buffa::MessageFullName form is preserved nowhere, so it's a clean rename from your branch's perspective.

- `message_name_impl()`: strip the `"<package>."` prefix atomically. The
  two-step `strip_prefix(package)` then `strip_prefix(".")` would
  partial-match a prefix-overlapping package (`package = "foo"` against
  `proto_fqn = "food.Bar"`), leave `NAME = "food.Bar"`, and silently
  violate the documented `PACKAGE + "." + NAME == FULL_NAME` invariant.
  An atomic `strip_prefix("foo.")` only matches the real prefix.

- `MessageName::NAME` doc: clarify that `NAME` is the dotted "type name
  relative to the package" (matching `prost::Name::NAME`), *not*
  `DescriptorProto.name` which is only the leaf segment for nested
  types. Add a runnable doctest showing the `T: MessageName` bound
  pattern.

- Codegen tests: `test_message_name_consts` pins all four consts for
  packaged + nested + empty-package + nested-in-empty-package shapes,
  including the `PACKAGE + "." + NAME == FULL_NAME` invariant the
  atomic strip protects. `gated_view_module_is_cfg_blocked` now also
  asserts the view's `impl MessageName` lives in the `View` content
  file (gated behind `feature = "views"`) and the owned impl lives in
  the `Owned` content file (unconditional).
@iainmcgin iainmcgin merged commit 43e7ad0 into anthropics:main May 14, 2026
7 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators May 14, 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.

2 participants