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
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),

## [Unreleased]

### Added

- **`[debug_redact = true]` is honored in generated `Debug` impls.** Fields
carrying the standard `debug_redact` field option print `[REDACTED]` instead
of their value in the owned message's `Debug` impl, and oneof enums, view
structs, and view-oneof enums containing such fields swap their
`#[derive(Debug)]` for a generated impl that redacts those fields/variants.
Output for messages without the annotation is unchanged. Note this covers
`Debug` formatting only — text-format and JSON serialization are
intentionally unaffected. A view struct containing a redacted field now
lists proto fields only in its `Debug` output (matching owned messages), so
`__buffa_unknown_fields` / phantom internals no longer appear there.
The reflective `DynamicMessage` `Debug` impl honors the option as well,
printing `[REDACTED]` in place of the value of any field whose descriptor
carries it.

### Changed

- **`HasMessageView` carries a `#[diagnostic::on_unimplemented]` hint.** When a
Expand All @@ -18,6 +34,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
consumers such as connect-rust rely on this trait for their request
wrappers, so the notes land directly in the consumer's build output.

### Fixed

- The owned message `Debug` impl now labels keyword-named fields without the
raw-identifier prefix (`type` instead of `r#type`), matching what
`#[derive(Debug)]` prints and what the view `Debug` impl emits.

## [0.7.0] - 2026-05-28

This release is a minor bump under the
Expand Down
8 changes: 5 additions & 3 deletions buffa-build/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -657,9 +657,11 @@ impl Config {
/// # Pitfalls
///
/// buffa already emits `#[derive(Clone, PartialEq)]` on messages and
/// `#[derive(Clone, PartialEq, Debug)]` on oneofs; adding a duplicate
/// derive via `type_attribute(".", "#[derive(Clone)]")` produces a
/// compile error in the generated code.
/// `#[derive(Clone, PartialEq, Debug)]` on oneofs (oneofs with a
/// `[debug_redact = true]` variant get a generated `Debug` impl instead
/// of the `Debug` derive); adding a duplicate derive via
/// `type_attribute(".", "#[derive(Clone)]")` produces a compile error in
/// the generated code.
///
/// # Example
///
Expand Down
54 changes: 48 additions & 6 deletions buffa-codegen/src/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,12 @@ fn generate_message_with_nesting(
.collect();
let direct_fields: Vec<&TokenStream> = generated_fields.iter().map(|f| &f.tokens).collect();

// Collect field identifiers for the manual Debug impl (excludes __buffa_ internals).
let mut debug_field_idents: Vec<&Ident> = generated_fields.iter().map(|f| &f.ident).collect();
// Collect (identifier, redacted) pairs for the manual Debug impl
// (excludes __buffa_ internals).
let mut debug_fields: Vec<(&Ident, bool)> = generated_fields
.iter()
.map(|f| (&f.ident, f.debug_redact))
.collect();

let setter_methods: TokenStream = generated_fields
.iter()
Expand Down Expand Up @@ -270,7 +274,8 @@ fn generate_message_with_nesting(
})
.collect();
let oneof_struct_fields: Vec<&TokenStream> = oneof_generated.iter().map(|(t, _)| t).collect();
debug_field_idents.extend(oneof_generated.iter().map(|(_, id)| id));
// Redaction of oneof payloads is handled by the oneof enum's own Debug impl.
debug_fields.extend(oneof_generated.iter().map(|(_, id)| (id, false)));

// When JSON is on, `__buffa_unknown_fields` becomes a `#[serde(flatten)]`
// newtype wrapper whose Serialize/Deserialize route through the extension
Expand Down Expand Up @@ -723,14 +728,30 @@ fn generate_message_with_nesting(
};

// Generate a manual Debug impl that excludes internal __buffa_ fields.
// Fields marked `[debug_redact = true]` print DEBUG_REDACT_PLACEHOLDER
// instead of their value, mirroring protobuf's DebugString redaction.
let struct_name_str = name_ident.to_string();
let debug_field_names: Vec<String> =
debug_field_idents.iter().map(|id| id.to_string()).collect();
// Labels match what `#[derive(Debug)]` prints: raw-ident fields (`r#type`)
// show as `type`, consistent with the view struct's Debug impl.
let debug_field_names: Vec<String> = debug_fields
.iter()
.map(|(id, _)| id.to_string().trim_start_matches("r#").to_string())
.collect();
let debug_field_values: Vec<TokenStream> = debug_fields
.iter()
.map(|(id, redacted)| {
if *redacted {
quote! { &::core::format_args!(#DEBUG_REDACT_PLACEHOLDER) }
} else {
quote! { &self.#id }
}
})
.collect();
let debug_impl = quote! {
impl ::core::fmt::Debug for #name_ident {
fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
f.debug_struct(#struct_name_str)
#(.field(#debug_field_names, &self.#debug_field_idents))*
#(.field(#debug_field_names, #debug_field_values))*
.finish()
}
}
Expand Down Expand Up @@ -1565,6 +1586,9 @@ struct GeneratedField {
tokens: TokenStream,
ident: Ident,
setter: Option<SetterInfo>,
/// Field carries `[debug_redact = true]`; the generated `Debug` impl
/// prints [`DEBUG_REDACT_PLACEHOLDER`] instead of the value.
debug_redact: bool,
}

fn generate_field(
Expand Down Expand Up @@ -1677,6 +1701,7 @@ fn generate_field(
tokens,
ident: rust_name,
setter,
debug_redact: is_debug_redacted(field),
}))
}

Expand All @@ -1687,6 +1712,23 @@ pub(crate) fn is_map_field(
field.r#type.unwrap_or_default() == Type::TYPE_MESSAGE && find_map_entry(msg, field).is_some()
}

/// Marker emitted by generated `Debug` impls in place of values whose field is
/// annotated `[debug_redact = true]`. Interpolated into `format_args!` as the
/// format string, so it must not contain `{` or `}`.
pub(crate) const DEBUG_REDACT_PLACEHOLDER: &str = "[REDACTED]";

/// True when the field carries `[debug_redact = true]`, i.e. its value must
/// not appear in generated `Debug` output.
pub(crate) fn is_debug_redacted(
field: &crate::generated::descriptor::FieldDescriptorProto,
) -> bool {
field
.options
.as_option()
.and_then(|o| o.debug_redact)
.unwrap_or(false)
}

/// Find the synthetic map-entry nested message for a map field.
///
/// Returns `None` if the field is not a map field (no matching nested type
Expand Down
50 changes: 49 additions & 1 deletion buffa-codegen/src/oneof.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ struct VariantInfo {
/// Owned string representation for a `string` variant (default `String`).
/// Drives both the variant type and the EcoString arbitrary shim.
string_repr: crate::StringRepr,
/// Variant's field carries `[debug_redact = true]`; the enum's `Debug`
/// impl prints a placeholder instead of the payload.
debug_redact: bool,
}

#[allow(clippy::too_many_arguments)]
Expand Down Expand Up @@ -151,6 +154,7 @@ fn collect_variant_info(
custom_attrs,
use_bytes,
string_repr,
debug_redact: crate::message::is_debug_redacted(field),
})
})
.collect()
Expand Down Expand Up @@ -303,15 +307,59 @@ pub fn generate_oneof_enum(
let custom_type_attrs =
CodeGenContext::matching_attributes(&ctx.config.type_attributes, &oneof_fqn)?;

// Variants whose field is `[debug_redact = true]` print a placeholder
// instead of their payload. The `Debug` derive is swapped for a manual
// impl only when at least one variant is redacted, so unaffected oneofs
// keep byte-identical output.
let any_redacted = variants_info.iter().any(|v| v.debug_redact);
let (debug_derive, debug_impl) = if any_redacted {
let placeholder = crate::message::DEBUG_REDACT_PLACEHOLDER;
let arms: Vec<TokenStream> = variants_info
.iter()
.map(|v| {
let ident = &v.variant_ident;
let name = ident.to_string();
if v.debug_redact {
quote! {
Self::#ident(_) => f
.debug_tuple(#name)
.field(&::core::format_args!(#placeholder))
.finish(),
}
} else {
quote! {
Self::#ident(value) => f.debug_tuple(#name).field(value).finish(),
}
}
})
.collect();
(
quote! { #[derive(Clone, PartialEq)] },
quote! {
impl ::core::fmt::Debug for #rust_enum_ident {
fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
match self {
#(#arms)*
}
}
}
},
)
} else {
(quote! { #[derive(Clone, PartialEq, Debug)] }, quote! {})
};

Ok(quote! {
#oneof_doc
#[derive(Clone, PartialEq, Debug)]
#debug_derive
#arbitrary_derive
#custom_type_attrs
pub enum #rust_enum_ident {
#(#variants,)*
}

#debug_impl

impl ::buffa::Oneof for #rust_enum_ident {}

#(#from_impls)*
Expand Down
Loading
Loading