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
3 changes: 3 additions & 0 deletions .changes/unreleased/changed-20260627-000610.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
kind: Changed
body: 'Documented the struct evolution policy for generated types: exhaustive struct literals and destructuring of generated message/view structs are not covered by semver. See the `Message` trait docs. (#202, #244)'
time: 2026-06-27T00:06:10.071892619Z
22 changes: 22 additions & 0 deletions buffa/src/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,28 @@ impl<'a> DecodeContext<'a> {
/// in the message — so messages can be placed in an `Arc` and shared across
/// threads freely. `merge` requires `&mut self`, so mutation is exclusive.
///
/// # Struct evolution policy
///
/// Generated message structs (and their [`MessageView`](crate::MessageView) /
/// [`LazyMessageView`](crate::LazyMessageView) counterparts) may gain fields
/// across releases — both when the source `.proto` schema evolves and when
/// buffa adds internal bookkeeping such as `__buffa_unknown_fields` or the
/// required-field presence bitmaps. **Exhaustive struct literals and
/// exhaustive destructuring patterns are not covered by buffa's semver
/// guarantees**: code that names every field will fail to compile when a field
/// is added, and that breakage is not considered a breaking change.
///
/// The forward-compatible ways to construct a generated struct are:
///
/// - decode it from bytes;
/// - struct-update syntax over the default: `Foo { x, y, ..Default::default() }`;
/// - start from `Foo::default()` and assign fields (or call generated `with_*`
/// setters when `generate_with_setters` is enabled).
///
/// The structs are deliberately *not* `#[non_exhaustive]`, so struct-update
/// syntax remains available from downstream crates; this policy is a documented
/// contract rather than a compiler-enforced one.
///
/// [`SizeCache`]: crate::SizeCache
pub trait Message: DefaultInstance + Clone + PartialEq + Send + Sync {
/// Compute the encoded byte size of this message, recording nested
Expand Down
6 changes: 6 additions & 0 deletions buffa/src/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ use bytes::{BufMut, Bytes};
///
/// The lifetime `'a` ties the view to the input buffer — the view cannot
/// outlive the buffer it was decoded from.
///
/// Generated view structs may gain fields across releases; see the
/// [struct evolution policy on `Message`](crate::Message#struct-evolution-policy).
pub trait MessageView<'a>: Sized {
/// The corresponding owned message type.
type Owned: crate::Message;
Expand Down Expand Up @@ -931,6 +934,9 @@ impl<V: Eq + DefaultViewInstance> Eq for MessageFieldView<V> {}
/// [`LazyMessageFieldView`] / [`LazyRepeatedView`]) and decoded only on
/// access. Deferred validation is therefore visible in the type and trait
/// bound — generic code over `MessageView` never silently inherits it.
///
/// Generated lazy-view structs may gain fields across releases; see the
/// [struct evolution policy on `Message`](crate::Message#struct-evolution-policy).
pub trait LazyMessageView<'a>: Sized {
/// The corresponding owned message type.
type Owned: crate::Message;
Expand Down
7 changes: 7 additions & 0 deletions docs/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -673,6 +673,13 @@ Key design choices:
- **`MessageField<T>`** for sub-message fields (not `Option<Box<T>>`)
- **`EnumValue<E>`** for open enum fields (not raw `i32`)
- **`__buffa_unknown_fields`** preserves fields from newer schema versions
- **Struct evolution policy**: generated message and view structs may gain
fields as schemas evolve or buffa adds internal bookkeeping. Construct values
by decoding, with `Foo { x, ..Default::default() }`, or by starting from
`Foo::default()` and assigning fields; exhaustive struct literals and
destructuring are not covered by buffa's semver guarantees. See the
[`Message` trait documentation](https://docs.rs/buffa/latest/buffa/trait.Message.html#struct-evolution-policy)
for the full policy.
- **Module nesting** for nested message types (`outer::Inner`, not `OuterInner`)
- **No serialization state** — sizes live in an external [`SizeCache`](https://docs.rs/buffa/latest/buffa/struct.SizeCache.html), so the struct holds only its proto fields plus the unknown-fields plumbing, with no interior mutability

Expand Down
Loading