diff --git a/.changes/unreleased/changed-20260627-000610.yaml b/.changes/unreleased/changed-20260627-000610.yaml new file mode 100644 index 00000000..2129c0ce --- /dev/null +++ b/.changes/unreleased/changed-20260627-000610.yaml @@ -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 diff --git a/buffa/src/message.rs b/buffa/src/message.rs index 4bce3cf7..a6db97f6 100644 --- a/buffa/src/message.rs +++ b/buffa/src/message.rs @@ -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 diff --git a/buffa/src/view.rs b/buffa/src/view.rs index 08248f91..afc3a58c 100644 --- a/buffa/src/view.rs +++ b/buffa/src/view.rs @@ -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; @@ -931,6 +934,9 @@ impl Eq for MessageFieldView {} /// [`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; diff --git a/docs/guide.md b/docs/guide.md index d2e34acf..437bea10 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -673,6 +673,13 @@ Key design choices: - **`MessageField`** for sub-message fields (not `Option>`) - **`EnumValue`** 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