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
6 changes: 3 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,11 @@ thiserror = { version = "2", default-features = false }
# `default-features = false` so they stay `no_std`; the relevant
# `serde`/`arbitrary`/`std` sub-features are turned on by the matching `buffa`
# feature (see buffa/Cargo.toml).
smol_str = { version = "0.3", default-features = false }
#
# `smol_str` is capped below 0.3.4: that release raised its MSRV to 1.89, above
# buffa's 1.85. 0.3.2 (no declared MSRV) keeps the `serde`/`arbitrary` features
# we need. Relax the cap when buffa's MSRV reaches 1.89.
smol_str = { version = ">=0.3, <0.3.4", default-features = false }
ecow = { version = "0.2", default-features = false }
compact_str = { version = "0.9", default-features = false }
prettyplease = "0.2"
Expand Down
57 changes: 57 additions & 0 deletions buffa-build/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ use buffa::Message;
use buffa_codegen::generated::descriptor::FileDescriptorSet;

use buffa_codegen::CodeGenConfig;
#[doc(inline)]
pub use buffa_codegen::StringRepr;

/// How to produce a `FileDescriptorSet` from `.proto` files.
#[derive(Debug, Clone, Default)]
Expand Down Expand Up @@ -439,6 +441,61 @@ impl Config {
self
}

/// Map `string` fields to a [`StringRepr`] other than `String` for the
/// given proto path prefixes. The string counterpart to
/// [`use_bytes_type_in`](Self::use_bytes_type_in).
///
/// Each path is a fully-qualified proto path prefix (e.g.
/// `".my.pkg.MyMessage.name"` for one field, `".my.pkg"` for a package).
///
/// Rules accumulate and the **last** matching rule wins. Order therefore
/// matters: call [`string_type`](Self::string_type) (the broad default)
/// *first*, then `string_type_in` for narrower overrides — a broad rule
/// added after a specific one will shadow it.
///
/// The downstream crate must enable the selected type's `buffa` feature
/// (`smol_str`, `ecow`, or `compact_str`); otherwise the generated
/// `::buffa::<crate>::<Type>` references fail to resolve.
///
/// Only the owned Rust type changes: the wire format is unchanged, view
/// types still borrow `&str`, and `map<_, string>` keys and values stay
/// `String`.
///
/// # Example
///
/// ```rust,ignore
/// use buffa_build::StringRepr;
/// buffa_build::Config::new()
/// .string_type(StringRepr::SmolStr) // broad default first
/// .string_type_in(StringRepr::CompactString, &[".my.pkg.Msg.body"]) // narrow override
/// .files(&["proto/my_service.proto"])
/// .includes(&["proto/"])
/// .compile()
/// .unwrap();
/// ```
#[must_use]
pub fn string_type_in(mut self, repr: StringRepr, paths: &[impl AsRef<str>]) -> Self {
self.codegen_config
.string_fields
.extend(paths.iter().map(|p| (p.as_ref().to_string(), repr)));
self
}

/// Map every `string` field in all messages to the given [`StringRepr`].
///
/// Convenience for `.string_type_in(repr, &["."])`. Call this *before* any
/// [`string_type_in`](Self::string_type_in) overrides, since the last
/// matching rule wins (a `"."` rule added later shadows earlier specific
/// rules). `map<_, string>` keys and values stay `String`, and the
/// downstream crate must enable the selected type's `buffa` feature.
#[must_use]
pub fn string_type(mut self, repr: StringRepr) -> Self {
self.codegen_config
.string_fields
.push((".".to_string(), repr));
self
}

/// Add a custom attribute to generated types (messages and enums)
/// matching a proto path prefix.
///
Expand Down
18 changes: 18 additions & 0 deletions buffa-codegen/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,24 @@ impl<'a> CodeGenContext<'a> {
.iter()
.any(|prefix| matches_proto_prefix(prefix, field_fqn))
}

/// Resolve the [`StringRepr`](crate::StringRepr) for a `string` field at the
/// given proto path.
///
/// `field_fqn` is the fully-qualified proto field path, e.g.
/// `".my.pkg.MyMessage.name"`. Rules in `config.string_fields` are matched
/// with the same proto-segment-aware prefix logic as
/// [`use_bytes_type`](Self::use_bytes_type); the **last** matching rule wins,
/// letting a specific override follow a broad default. Fields matching no
/// rule use [`StringRepr::String`](crate::StringRepr::String).
pub fn string_repr(&self, field_fqn: &str) -> crate::StringRepr {
self.config
.string_fields
.iter()
.rev()
.find(|(prefix, _)| matches_proto_prefix(prefix, field_fqn))
.map_or(crate::StringRepr::default(), |(_, repr)| *repr)
}
}

/// Scope-local context for code generation within a message.
Expand Down
10 changes: 9 additions & 1 deletion buffa-codegen/src/defaults.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pub fn parse_default_value(
current_package: &str,
features: &ResolvedFeatures,
nesting: usize,
string_repr: crate::StringRepr,
) -> Result<Option<TokenStream>, CodeGenError> {
use crate::generated::descriptor::field_descriptor_proto::Type;

Expand Down Expand Up @@ -90,8 +91,15 @@ pub fn parse_default_value(
// the proto source literal is valid UTF-8 by definition.
if crate::impl_message::effective_type(ctx, field, features) == Type::TYPE_BYTES {
quote! { ::buffa::alloc::string::String::from(#default_str).into_bytes() }
} else {
} else if string_repr.is_default() {
quote! { ::buffa::alloc::string::String::from(#default_str) }
} else {
// Non-default string types: convert via From<String>. The
// surrounding context (Default initializer / clear assignment)
// pins the target type, so `Into::into` infers it.
quote! {
::core::convert::Into::into(::buffa::alloc::string::String::from(#default_str))
}
}
}
Type::TYPE_BYTES => {
Expand Down
32 changes: 20 additions & 12 deletions buffa-codegen/src/extension.rs
Original file line number Diff line number Diff line change
Expand Up @@ -282,18 +282,26 @@ fn default_fn_tokens(
}

let value_ty = codec_value_type(ty);
let default_expr =
crate::defaults::parse_default_value(field, ctx, current_package, features, nesting)?
.ok_or_else(|| {
// default_value was non-empty but parse returned None —
// happens when field_presence ≠ Explicit (shouldn't for
// extensions, which are always explicit-presence per
// protocolbuffers/protobuf#8234) or for an unhandled type.
CodeGenError::Other(format!(
"extension `{proto_name}`: could not parse default_value `{}` for type {ty:?}",
field.default_value.as_deref().unwrap_or_default()
))
})?;
// Extension string fields always use `String` (not remapped by the
// `string_type` knob), so their default keeps the `String` form.
let default_expr = crate::defaults::parse_default_value(
field,
ctx,
current_package,
features,
nesting,
crate::StringRepr::String,
)?
.ok_or_else(|| {
// default_value was non-empty but parse returned None —
// happens when field_presence ≠ Explicit (shouldn't for
// extensions, which are always explicit-presence per
// protocolbuffers/protobuf#8234) or for an unhandled type.
CodeGenError::Other(format!(
"extension `{proto_name}`: could not parse default_value `{}` for type {ty:?}",
field.default_value.as_deref().unwrap_or_default()
))
})?;

// String::from / vec![...] allocate → can't be const. Everything else
// (integer/float/bool literals) is const-evaluable.
Expand Down
97 changes: 80 additions & 17 deletions buffa-codegen/src/impl_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -849,6 +849,20 @@ pub(crate) fn field_uses_bytes(ctx: &CodeGenContext, proto_fqn: &str, field_name
ctx.use_bytes_type(&field_fqn)
}

/// Resolve the [`StringRepr`](crate::StringRepr) for a `string`-typed field.
///
/// `proto_fqn` is the fully-qualified message name (no leading dot). Matched
/// against `config.string_fields` as `".my.pkg.Msg.field"`. Returns
/// [`StringRepr::String`](crate::StringRepr::String) for fields with no rule.
pub(crate) fn field_string_repr(
ctx: &CodeGenContext,
proto_fqn: &str,
field_name: &str,
) -> crate::StringRepr {
let field_fqn = format!(".{}.{}", proto_fqn, field_name);
ctx.string_repr(&field_fqn)
}

fn scalar_clear_stmt(
field: &FieldDescriptorProto,
ctx: &CodeGenContext,
Expand All @@ -873,14 +887,27 @@ fn scalar_clear_stmt(

// If the field has a custom default value (proto2), use it instead of
// the type's zero value so that clear() matches Default::default().
if let Some(default_expr) =
crate::defaults::parse_default_value(field, ctx, current_package, features, nesting)?
{
if let Some(default_expr) = crate::defaults::parse_default_value(
field,
ctx,
current_package,
features,
nesting,
field_string_repr(ctx, proto_fqn, field_name),
)? {
return Ok(quote! { self.#ident = #default_expr; });
}

match ty {
Type::TYPE_STRING => Ok(quote! { self.#ident.clear(); }),
Type::TYPE_STRING => {
// Non-default string types may be immutable (no `clear()`), so
// reset to the default value uniformly.
if field_string_repr(ctx, proto_fqn, field_name).is_default() {
Ok(quote! { self.#ident.clear(); })
} else {
Ok(quote! { self.#ident = ::core::default::Default::default(); })
}
}
Type::TYPE_BYTES => {
// bytes::Bytes is immutable (no clear()), so reassign.
if use_bytes {
Expand Down Expand Up @@ -1463,17 +1490,19 @@ fn scalar_write_to_stmt(
///
/// Emits `field_number => { wire_check; self.field = Some(decoded_value); }`.
/// Proto3 optional fields and proto2 optional non-message fields use this path.
#[allow(clippy::too_many_arguments)]
fn explicit_presence_merge_arm(
ident: &Ident,
field_number: u32,
ty: Type,
features: &ResolvedFeatures,
wire_check: &TokenStream,
use_bytes: bool,
string_repr: crate::StringRepr,
preserve_unknown_fields: bool,
) -> TokenStream {
match ty {
Type::TYPE_STRING => quote! {
Type::TYPE_STRING if string_repr.is_default() => quote! {
#field_number => {
#wire_check
::buffa::types::merge_string(
Expand All @@ -1482,6 +1511,14 @@ fn explicit_presence_merge_arm(
)?;
}
},
Type::TYPE_STRING => quote! {
#field_number => {
#wire_check
self.#ident = ::core::option::Option::Some(
::buffa::types::decode_string_to(buf)?
);
}
},
Type::TYPE_BYTES => {
if use_bytes {
// bytes::Bytes is immutable — can't merge in place. Replace
Expand Down Expand Up @@ -1560,6 +1597,7 @@ fn scalar_merge_arm(
let field_number = validated_field_number(field)?;
let ty = effective_type(ctx, field, features);
let use_bytes = ty == Type::TYPE_BYTES && field_uses_bytes(ctx, proto_fqn, field_name);
let string_repr = field_string_repr(ctx, proto_fqn, field_name);
let ident = make_field_ident(field_name);
let wire_type = wire_type_token(ty);
let expected_byte = wire_type_byte(ty);
Expand All @@ -1575,6 +1613,7 @@ fn scalar_merge_arm(
features,
&wire_check,
use_bytes,
string_repr,
preserve_unknown_fields,
));
}
Expand All @@ -1589,10 +1628,21 @@ fn scalar_merge_arm(
// decode_bytes which always allocate a fresh Vec/String.
match ty {
Type::TYPE_STRING => {
return Ok(quote! {
#field_number => {
#wire_check
::buffa::types::merge_string(&mut self.#ident, buf)?;
return Ok(if string_repr.is_default() {
quote! {
#field_number => {
#wire_check
::buffa::types::merge_string(&mut self.#ident, buf)?;
}
}
} else {
// Non-default string types are constructed fresh on decode; the
// in-place `merge_string` allocation reuse is `String`-only.
quote! {
#field_number => {
#wire_check
self.#ident = ::buffa::types::decode_string_to(buf)?;
}
}
});
}
Expand Down Expand Up @@ -1989,7 +2039,10 @@ fn repeated_merge_arm(
2u8,
);
let decode_expr = match ty {
Type::TYPE_STRING => quote! { ::buffa::types::decode_string(buf)? },
Type::TYPE_STRING if field_string_repr(ctx, proto_fqn, field_name).is_default() => {
quote! { ::buffa::types::decode_string(buf)? }
}
Type::TYPE_STRING => quote! { ::buffa::types::decode_string_to(buf)? },
Type::TYPE_BYTES => {
if use_bytes {
quote! { ::buffa::types::decode_bytes_to_bytes(buf)? }
Expand Down Expand Up @@ -2275,19 +2328,27 @@ fn oneof_merge_arm(
features: &ResolvedFeatures,
preserve_unknown_fields: bool,
use_bytes: bool,
string_repr: crate::StringRepr,
) -> TokenStream {
let wire_type = wire_type_token(ty);
let wire_byte = wire_type_byte(ty);
let wire_check = wire_type_check(field_number, &wire_type, wire_byte);
match ty {
Type::TYPE_STRING => quote! {
#field_number => {
#wire_check
self.#field_ident = ::core::option::Option::Some(
#enum_ident::#variant_ident(::buffa::types::decode_string(buf)?)
);
Type::TYPE_STRING => {
let decoded = if string_repr.is_default() {
quote! { ::buffa::types::decode_string(buf)? }
} else {
quote! { ::buffa::types::decode_string_to(buf)? }
};
quote! {
#field_number => {
#wire_check
self.#field_ident = ::core::option::Option::Some(
#enum_ident::#variant_ident(#decoded)
);
}
}
},
}
Type::TYPE_BYTES => {
let decoded = if use_bytes {
quote! { ::buffa::types::decode_bytes_to_bytes(buf)? }
Expand Down Expand Up @@ -2427,6 +2488,7 @@ fn generate_oneof_impls(
));
let field_features = crate::features::resolve_field(ctx, field, features);
let use_bytes = ty == Type::TYPE_BYTES && field_uses_bytes(ctx, proto_fqn, field_name);
let string_repr = field_string_repr(ctx, proto_fqn, field_name);
merge_arm_list.push(oneof_merge_arm(
&field_ident,
&qualified_enum,
Expand All @@ -2436,6 +2498,7 @@ fn generate_oneof_impls(
&field_features,
preserve_unknown_fields,
use_bytes,
string_repr,
));
}

Expand Down
Loading
Loading