Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,36 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),

### Added

- **`buffa::MessageName` trait exposes a generated message's protobuf
identifiers as compile-time `&'static str` constants.** Codegen emits
`impl MessageName for #Msg` (and `for #MsgView<'a>`) with four consts:
`PACKAGE` (`"my.pkg"`, empty for the unnamed root package), `NAME`
(`"Outer.Inner"` — unqualified, with `.` between nesting levels),
`FULL_NAME` (`"my.pkg.Outer.Inner"`), and `TYPE_URL`
(`"type.googleapis.com/my.pkg.Outer.Inner"` — the
`google.protobuf.Any.type_url` form). All four are computed at codegen
time as string literals, so there's no runtime allocation or
concatenation — unlike `prost::Name`, whose `full_name()` and
`type_url()` are runtime `format!` calls. `PACKAGE` and `NAME` are
separate consts 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`).

The trait has no supertrait — it doesn't reach into the wire codec —
so view types implement it too: a generic event-sourcing registry can
bound on `T: MessageName` and dispatch zero-copy views and owned
messages identically. Useful for type-erased registries, logging, and
any code that needs the protobuf name without the descriptor machinery.
The inherent `Foo::TYPE_URL` const generated since 0.4.0 is unchanged
and equal to `<Foo as MessageName>::TYPE_URL`; for messages that also
implement `ExtensionSet`, `FULL_NAME` is equal to
`ExtensionSet::PROTO_FQN` (all derive from the same codegen source).
`MessageName` is **not** object-safe (associated `const` only) — use it
as a bound, not `dyn MessageName`. Migrating from `prost::Name`: rename
the bound and replace runtime `M::full_name()` / `M::type_url()` calls
with the consts. ([#108](https://github.com/anthropics/buffa/pull/108),
by @yordis)

- **`buf.build/anthropics/buffa` is published to the public Buf Schema
Registry.** `buf generate` can now reference `protoc-gen-buffa` as a
`remote:` plugin with no local install: `remote: buf.build/anthropics/buffa`
Expand Down
57 changes: 57 additions & 0 deletions buffa-codegen/src/impl_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,54 @@ fn message_uses_size_cache(

/// Generate `impl DefaultInstance` and `impl Message` for a message.
///
/// Emit `impl #generics ::buffa::MessageName for #ty { … }`.
///
/// `generics` is the impl-side generic parameter list (`<'a>` for the
/// view type, empty for the owned message). `ty` is the implementing
/// type *with* any generics applied (`Foo` or `FooView<'a>`).
///
/// All four consts are computed at codegen time as string literals so
/// `T::FULL_NAME` etc. are zero-cost at runtime — no `format!`,
/// `concat!`, or lazy static. `PACKAGE` and `NAME` are split here rather
/// than left to the consumer because the dotted `FULL_NAME` cannot be
/// re-split unambiguously: `foo.Bar.Baz` could be package `foo.Bar` +
/// message `Baz`, or package `foo` + nested `Bar.Baz`. Codegen knows
/// which.
pub(crate) fn message_name_impl(
current_package: &str,
proto_fqn: &str,
generics: &TokenStream,
ty: &TokenStream,
) -> TokenStream {
let name = if current_package.is_empty() {
proto_fqn.to_string()
} else {
// Strip `"<package>."` atomically — a two-step
// `strip_prefix(package)` then `strip_prefix(".")` would
// partial-match a prefix-overlapping package (`package = "foo"`
// against `proto_fqn = "food.Bar"`) and silently violate the
// documented `PACKAGE + "." + NAME == FULL_NAME` invariant.
//
// `proto_fqn` is always `"<package>.<rest>"` for a non-empty
// package (it's built by joining message segments onto the
// package), so the strip should never fail. Fall back
// defensively rather than panic on a malformed descriptor.
proto_fqn
.strip_prefix(&format!("{current_package}."))
.unwrap_or(proto_fqn)
.to_string()
};
let type_url = format!("type.googleapis.com/{proto_fqn}");
quote! {
impl #generics ::buffa::MessageName for #ty {
const PACKAGE: &'static str = #current_package;
const NAME: &'static str = #name;
const FULL_NAME: &'static str = #proto_fqn;
const TYPE_URL: &'static str = #type_url;
}
}
}

/// `preserve_unknown_fields`: when `true`, the generated merge collects
/// unknown fields into `self.__buffa_unknown_fields` and both `compute_size` and
/// `write_to` include them.
Expand Down Expand Up @@ -543,6 +591,13 @@ pub fn generate_message_impl(
quote! {}
};

let message_name_impl = message_name_impl(
current_package,
proto_fqn,
&quote! {},
&quote! { #name_ident },
);

Ok(quote! {
impl ::buffa::DefaultInstance for #name_ident {
fn default_instance() -> &'static Self {
Expand All @@ -552,6 +607,8 @@ pub fn generate_message_impl(
}
}

#message_name_impl

impl ::buffa::Message for #name_ident {
/// Returns the total encoded size in bytes.
///
Expand Down
39 changes: 39 additions & 0 deletions buffa-codegen/src/tests/feature_gating.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,45 @@ fn gated_view_module_is_cfg_blocked() {
squashed.contains(r#"#[cfg(feature = "views")] pub mod view"#),
"the views cfg must precede `pub mod view`: {content}"
);
// The view's `impl MessageName for FooView<'a>` lives in the
// `<stem>.__view.rs` content file, which the stitcher `include!`s
// *inside* the `#[cfg(feature = "views")] pub mod view` block, so
// it disappears with the feature. The owned-message impl is in the
// `<stem>.rs` content file (unconditional). Pin both: a refactor
// that emits the view impl into a non-View content file would ship
// a name-resolution error against a cfg'd-out type.
let cfg = CodeGenConfig {
generate_json: true,
generate_views: true,
generate_text: false,
preserve_unknown_fields: true,
gate_impls_on_crate_features: true,
..CodeGenConfig::default()
};
let files =
generate(&[fixture()], &["gated.proto".to_string()], &cfg).expect("should generate");
for f in &files {
let has_view_impl = f.content.contains("MessageName for OuterView");
let has_owned_impl = squash(&f.content).contains("impl ::buffa::MessageName for Outer ");
match f.kind {
GeneratedFileKind::View => assert!(
has_view_impl,
"view MessageName impl must be in the view content file ({})",
f.name
),
GeneratedFileKind::Owned => assert!(
has_owned_impl && !has_view_impl,
"owned content file must have the owned (not view) MessageName impl ({})",
f.name
),
_ => assert!(
!has_view_impl && !has_owned_impl,
"MessageName impl leaked into a {:?} file ({})",
f.kind,
f.name
),
}
}
}

#[test]
Expand Down
71 changes: 71 additions & 0 deletions buffa-codegen/src/tests/generation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1241,6 +1241,77 @@ fn test_type_url_doubly_nested() {
);
}

#[test]
fn test_message_name_consts() {
// The four `MessageName` consts must hold the documented invariant
// `PACKAGE + "." + NAME == FULL_NAME` (joining dot omitted when
// `PACKAGE` is empty), and `TYPE_URL == "type.googleapis.com/" +
// FULL_NAME`. The atomic-prefix-strip in `message_name_impl` makes
// a partial-match (`package = "foo"` against `proto_fqn =
// "food.Bar"`) impossible to slip through silently — pin the shape
// here so a refactor that re-introduces a two-step strip fails this
// test instead of shipping a broken `NAME`.
let mut file = proto3_file("named.proto");
file.package = Some("my.pkg".to_string());
file.message_type.push(DescriptorProto {
name: Some("Outer".to_string()),
nested_type: vec![DescriptorProto {
name: Some("Inner".to_string()),
..Default::default()
}],
..Default::default()
});
let files = generate(
&[file],
&["named.proto".to_string()],
&CodeGenConfig::default(),
)
.expect("should generate");
let content = joined(&files);
// Top-level: PACKAGE + "." + NAME == FULL_NAME.
for snippet in [
r#"const PACKAGE: &'static str = "my.pkg""#,
r#"const NAME: &'static str = "Outer""#,
r#"const FULL_NAME: &'static str = "my.pkg.Outer""#,
r#"const TYPE_URL: &'static str = "type.googleapis.com/my.pkg.Outer""#,
// Nested: NAME carries the dotted nesting path; PACKAGE stays
// at the proto package — NOT `DescriptorProto.name` (which is
// just `"Inner"`).
r#"const NAME: &'static str = "Outer.Inner""#,
r#"const FULL_NAME: &'static str = "my.pkg.Outer.Inner""#,
] {
assert!(content.contains(snippet), "missing `{snippet}`: {content}");
}

// Empty package: PACKAGE is "", NAME == FULL_NAME, no joining dot.
let mut root = proto3_file("root.proto");
root.message_type.push(DescriptorProto {
name: Some("Root".to_string()),
nested_type: vec![DescriptorProto {
name: Some("Leaf".to_string()),
..Default::default()
}],
..Default::default()
});
let files = generate(
&[root],
&["root.proto".to_string()],
&CodeGenConfig::default(),
)
.expect("should generate");
let content = joined(&files);
for snippet in [
r#"const PACKAGE: &'static str = """#,
r#"const NAME: &'static str = "Root""#,
r#"const FULL_NAME: &'static str = "Root""#,
r#"const NAME: &'static str = "Root.Leaf""#,
r#"const FULL_NAME: &'static str = "Root.Leaf""#,
r#"const TYPE_URL: &'static str = "type.googleapis.com/Root.Leaf""#,
] {
assert!(content.contains(snippet), "missing `{snippet}`: {content}");
}
}

#[test]
fn test_message_scalar_fields() {
let mut file = proto3_file("scalars.proto");
Expand Down
13 changes: 13 additions & 0 deletions buffa-codegen/src/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,17 @@ pub(crate) fn generate_view_with_nesting(
}
};

// The view participates in the same name-keyed registries as the
// owned message — a generic event-sourcing dispatch should not have
// to round-trip a zero-copy view through `to_owned_message()` just
// to read its FQN. Same consts, different `Self`.
let message_name_impl = crate::impl_message::message_name_impl(
current_package,
proto_fqn,
&quote! { <'a> },
&quote! { #view_ident<'a> },
);

let serialize_impl = if ctx.config.generate_json {
crate::feature_gates::cfg_block(
generate_view_serialize(
Expand Down Expand Up @@ -337,6 +348,8 @@ pub(crate) fn generate_view_with_nesting(

#serialize_impl

#message_name_impl

impl<'v> ::buffa::DefaultViewInstance for #view_ident<'v> {
fn default_view_instance<'a>() -> &'a Self
where
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,12 @@ impl<'__a> ::serde::Serialize for VersionView<'__a> {
__map.end()
}
}
impl<'a> ::buffa::MessageName for VersionView<'a> {
const PACKAGE: &'static str = "google.protobuf.compiler";
const NAME: &'static str = "Version";
const FULL_NAME: &'static str = "google.protobuf.compiler.Version";
const TYPE_URL: &'static str = "type.googleapis.com/google.protobuf.compiler.Version";
}
impl<'v> ::buffa::DefaultViewInstance for VersionView<'v> {
fn default_view_instance<'a>() -> &'a Self
where
Expand Down Expand Up @@ -655,6 +661,12 @@ impl<'__a> ::serde::Serialize for CodeGeneratorRequestView<'__a> {
__map.end()
}
}
impl<'a> ::buffa::MessageName for CodeGeneratorRequestView<'a> {
const PACKAGE: &'static str = "google.protobuf.compiler";
const NAME: &'static str = "CodeGeneratorRequest";
const FULL_NAME: &'static str = "google.protobuf.compiler.CodeGeneratorRequest";
const TYPE_URL: &'static str = "type.googleapis.com/google.protobuf.compiler.CodeGeneratorRequest";
}
impl<'v> ::buffa::DefaultViewInstance for CodeGeneratorRequestView<'v> {
fn default_view_instance<'a>() -> &'a Self
where
Expand Down Expand Up @@ -1000,6 +1012,12 @@ impl<'__a> ::serde::Serialize for CodeGeneratorResponseView<'__a> {
__map.end()
}
}
impl<'a> ::buffa::MessageName for CodeGeneratorResponseView<'a> {
const PACKAGE: &'static str = "google.protobuf.compiler";
const NAME: &'static str = "CodeGeneratorResponse";
const FULL_NAME: &'static str = "google.protobuf.compiler.CodeGeneratorResponse";
const TYPE_URL: &'static str = "type.googleapis.com/google.protobuf.compiler.CodeGeneratorResponse";
}
impl<'v> ::buffa::DefaultViewInstance for CodeGeneratorResponseView<'v> {
fn default_view_instance<'a>() -> &'a Self
where
Expand Down Expand Up @@ -1360,6 +1378,12 @@ pub mod code_generator_response {
__map.end()
}
}
impl<'a> ::buffa::MessageName for FileView<'a> {
const PACKAGE: &'static str = "google.protobuf.compiler";
const NAME: &'static str = "CodeGeneratorResponse.File";
const FULL_NAME: &'static str = "google.protobuf.compiler.CodeGeneratorResponse.File";
const TYPE_URL: &'static str = "type.googleapis.com/google.protobuf.compiler.CodeGeneratorResponse.File";
}
impl<'v> ::buffa::DefaultViewInstance for FileView<'v> {
fn default_view_instance<'a>() -> &'a Self
where
Expand Down
24 changes: 24 additions & 0 deletions buffa-descriptor/src/generated/google.protobuf.compiler.plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@ impl ::buffa::DefaultInstance for Version {
VALUE.get_or_init(|| ::buffa::alloc::boxed::Box::new(Self::default()))
}
}
impl ::buffa::MessageName for Version {
const PACKAGE: &'static str = "google.protobuf.compiler";
const NAME: &'static str = "Version";
const FULL_NAME: &'static str = "google.protobuf.compiler.Version";
const TYPE_URL: &'static str = "type.googleapis.com/google.protobuf.compiler.Version";
}
impl ::buffa::Message for Version {
/// Returns the total encoded size in bytes.
///
Expand Down Expand Up @@ -411,6 +417,12 @@ impl ::buffa::DefaultInstance for CodeGeneratorRequest {
VALUE.get_or_init(|| ::buffa::alloc::boxed::Box::new(Self::default()))
}
}
impl ::buffa::MessageName for CodeGeneratorRequest {
const PACKAGE: &'static str = "google.protobuf.compiler";
const NAME: &'static str = "CodeGeneratorRequest";
const FULL_NAME: &'static str = "google.protobuf.compiler.CodeGeneratorRequest";
const TYPE_URL: &'static str = "type.googleapis.com/google.protobuf.compiler.CodeGeneratorRequest";
}
impl ::buffa::Message for CodeGeneratorRequest {
/// Returns the total encoded size in bytes.
///
Expand Down Expand Up @@ -819,6 +831,12 @@ impl ::buffa::DefaultInstance for CodeGeneratorResponse {
VALUE.get_or_init(|| ::buffa::alloc::boxed::Box::new(Self::default()))
}
}
impl ::buffa::MessageName for CodeGeneratorResponse {
const PACKAGE: &'static str = "google.protobuf.compiler";
const NAME: &'static str = "CodeGeneratorResponse";
const FULL_NAME: &'static str = "google.protobuf.compiler.CodeGeneratorResponse";
const TYPE_URL: &'static str = "type.googleapis.com/google.protobuf.compiler.CodeGeneratorResponse";
}
impl ::buffa::Message for CodeGeneratorResponse {
/// Returns the total encoded size in bytes.
///
Expand Down Expand Up @@ -1367,6 +1385,12 @@ pub mod code_generator_response {
VALUE.get_or_init(|| ::buffa::alloc::boxed::Box::new(Self::default()))
}
}
impl ::buffa::MessageName for File {
const PACKAGE: &'static str = "google.protobuf.compiler";
const NAME: &'static str = "CodeGeneratorResponse.File";
const FULL_NAME: &'static str = "google.protobuf.compiler.CodeGeneratorResponse.File";
const TYPE_URL: &'static str = "type.googleapis.com/google.protobuf.compiler.CodeGeneratorResponse.File";
}
impl ::buffa::Message for File {
/// Returns the total encoded size in bytes.
///
Expand Down
Loading
Loading