buffa-descriptor 0.9.0

Protobuf descriptor types (FileDescriptorProto, DescriptorProto, ...) for buffa
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
//! The [`ReflectMessage`] trait, [`ReflectCow`], and the [`Reflectable`]
//! entry-point trait.
//!
//! `ReflectMessage` is **dyn-safe and storage-agnostic** by design. The
//! v1 implementation is map-backed [`DynamicMessage`](super::DynamicMessage);
//! a future vtable-backed implementation on generated types must slot in as
//! a *second* impl of the same trait, with no call-site changes. That
//! constraint dictates the signature shape:
//!
//! - Accessors take `&FieldDescriptor`, not a generic key — the vtable will
//!   index directly off the descriptor, the map will look up by number.
//! - Accessors return [`ValueRef<'_>`], not an associated type — both impls
//!   produce the same enum.
//! - `for_each_set` takes `&mut dyn FnMut`, not `impl FnMut` — `dyn` traits
//!   can't have generic methods.
//!
//! [`Reflectable`] is the codegen-emitted entry point: every generated message
//! gets an impl whenever any reflection is enabled, and the body varies by
//! [`ReflectMode`](super::ReflectMode). The call site is always
//! `foo.reflect().get(fd)`; bridge mode pays an encode/decode round-trip,
//! vtable mode is zero-cost. Flipping a message between modes requires no
//! diff at the call site.

use alloc::boxed::Box;
use alloc::string::{String, ToString};

use super::value::ValueRef;
use super::DynamicMessage;
use crate::{DescriptorPool, FieldDescriptor, MessageDescriptor, OneofDescriptor};

/// Errors returned by checked reflection mutation APIs.
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ReflectError {
    /// The supplied field descriptor is not a declared field or registered
    /// extension of the target message.
    ///
    /// Membership is identity-based, not structural: a descriptor that has
    /// the same name and number but came from a different
    /// [`DescriptorPool`] (e.g. two pools built from the same
    /// `FileDescriptorSet`) is still foreign. Always pass descriptors
    /// resolved from the message's own [`pool()`](ReflectMessage::pool).
    FieldNotMember {
        /// The message being mutated.
        message: String,
        /// The foreign descriptor's simple field name.
        field_name: String,
        /// The foreign descriptor's field number.
        number: u32,
    },
    /// The supplied value's runtime shape does not match the target field's
    /// descriptor.
    WrongValueKind {
        /// The message being mutated.
        message: String,
        /// The target field's simple field name.
        field_name: String,
        /// The target field's field number.
        number: u32,
        /// Human-readable descriptor shape expected by the field.
        expected: String,
        /// Human-readable runtime shape supplied by the caller.
        actual: String,
    },
}

impl ReflectError {
    pub(crate) fn field_not_member(message: &MessageDescriptor, field: &FieldDescriptor) -> Self {
        Self::FieldNotMember {
            message: message.full_name().to_string(),
            field_name: field.name().to_string(),
            number: field.number(),
        }
    }

    pub(crate) fn wrong_value_kind(
        message: &MessageDescriptor,
        field: &FieldDescriptor,
        expected: String,
        actual: String,
    ) -> Self {
        Self::WrongValueKind {
            message: message.full_name().to_string(),
            field_name: field.name().to_string(),
            number: field.number(),
            expected,
            actual,
        }
    }
}

impl core::fmt::Display for ReflectError {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        match self {
            Self::FieldNotMember {
                message,
                field_name,
                number,
            } => write!(
                f,
                "field descriptor {field_name:?} (#{number}) is not a member of {message}"
            ),
            Self::WrongValueKind {
                message,
                field_name,
                number,
                expected,
                actual,
            } => write!(
                f,
                "field {field_name:?} (#{number}) on {message} expects {expected}, got {actual}"
            ),
        }
    }
}

#[cfg(feature = "std")]
impl std::error::Error for ReflectError {}

/// Reflection over a protobuf message.
///
/// Implemented by [`DynamicMessage`] (map-backed) and, in vtable mode, by
/// generated message structs. See the module documentation for the dyn-safety
/// contract.
#[rustversion::attr(
    since(1.78),
    diagnostic::on_unimplemented(
        message = "`{Self}` does not implement `ReflectMessage`, which vtable-mode reflection requires on this embedded type",
        note = "if `{Self}` comes from another buffa-generated crate via an extern path (well-known types resolve to `buffa-types` by default), enable that crate's reflection feature, e.g. `buffa-types = {{ version = \"...\", features = [\"reflect\"] }}`",
        note = "view reflection cannot degrade across modes: every view type embedded in a vtable-mode view must itself be vtable-grade (owned messages degrade through `Reflectable::reflect()` instead)",
        note = "if `{Self}` is generated in this crate, its `build.rs` config must use `reflect_mode(ReflectMode::VTable)`"
    )
)]
pub trait ReflectMessage {
    /// The descriptor for this message type.
    fn message_descriptor(&self) -> &MessageDescriptor;

    /// The pool the descriptor lives in. Use this to dereference
    /// [`MessageIndex`](crate::MessageIndex) /
    /// [`EnumIndex`](crate::EnumIndex) from [`FieldKind`](crate::FieldKind),
    /// or `Arc::clone` it to construct sibling [`DynamicMessage`]s while
    /// navigating nested fields.
    fn pool(&self) -> &alloc::sync::Arc<DescriptorPool>;

    /// Get a field's value.
    ///
    /// For absent singular fields, returns the type's default value. For
    /// absent repeated/map fields, returns an empty container.
    ///
    /// # Panics
    ///
    /// May panic if `field` is not a member of this message's descriptor.
    /// Implementations are encouraged to `debug_assert!` rather than check
    /// in release.
    fn get(&self, field: &FieldDescriptor) -> ValueRef<'_>;

    /// Whether a field is present.
    ///
    /// For explicit-presence fields (proto2 `optional`/`required`, proto3
    /// `optional`, message-typed fields), this is "was a value written".
    /// For implicit-presence fields, this is "is non-default". For
    /// repeated/map fields, this is "non-empty".
    fn has(&self, field: &FieldDescriptor) -> bool;

    /// Visit every set field.
    ///
    /// "Set" follows the same semantics as [`Self::has`]. **Unknown fields
    /// are excluded** — they have no `FieldDescriptor`. Visit them
    /// separately via [`unknown_fields()`](Self::unknown_fields).
    fn for_each_set(&self, f: &mut dyn FnMut(&FieldDescriptor, ValueRef<'_>));

    /// The fields preserved from decode that the message's descriptor does
    /// not recognize.
    ///
    /// An unknown field carries only its field number and wire-level value
    /// (varint / fixed32 / fixed64 / length-delimited / group) — there is no
    /// descriptor, so no name and no proto type. A length-delimited payload
    /// is indistinguishably a string, a bytes field, a nested message, or a
    /// packed repeated scalar.
    ///
    /// This is on the trait (mirroring protobuf-go's `Message.GetUnknown`)
    /// so a recursive walk over `&dyn ReflectMessage` — an interceptor
    /// scanning every string in a request, a generic redactor — can reach
    /// the unknown fields of *nested* messages, not just the root. A walk
    /// that only visits [`for_each_set`](Self::for_each_set) silently skips
    /// any field added by a schema revision newer than this pool's.
    ///
    /// The default implementation returns an empty set, for implementations
    /// that do not preserve unknown fields.
    fn unknown_fields(&self) -> &buffa::UnknownFields {
        static EMPTY: buffa::UnknownFields = buffa::UnknownFields::new();
        &EMPTY
    }

    /// Which member of `oneof` is set, if any.
    ///
    /// The default implementation checks each member field's
    /// [`has()`](Self::has). Implementations that track oneof discriminants
    /// directly may override for `O(1)` dispatch.
    ///
    /// Synthetic oneofs (proto3 `optional`) have exactly one member; this
    /// returns it iff the field is present.
    ///
    /// `oneof` must come from `self`'s [`message_descriptor()`](Self::message_descriptor) —
    /// passing a `OneofDescriptor` from a different message returns `None`
    /// or an unrelated member, the same cross-descriptor hazard
    /// [`get()`](Self::get) documents.
    fn which_oneof(&self, oneof: &OneofDescriptor) -> Option<&FieldDescriptor> {
        let md = self.message_descriptor();
        for &i in oneof.field_indices() {
            if let Some(fd) = md.fields().get(i as usize) {
                if self.has(fd) {
                    return Some(fd);
                }
            }
        }
        None
    }

    /// Snapshot this message as an owned [`DynamicMessage`].
    ///
    /// For an already-dynamic message this is a clone; for a generated message
    /// (bridge or vtable mode) this is an encode/decode round-trip. Required
    /// rather than defaulted so that a `dyn ReflectMessage` can always be
    /// converted, which [`ReflectCow::to_dynamic`] relies on — and so a
    /// borrowed vtable handle can be promoted to an owned snapshot that
    /// outlives `self`.
    fn to_dynamic(&self) -> DynamicMessage;
}

/// Mutable reflection over a protobuf message.
///
/// Separated from [`ReflectMessage`] because read-only reflection is the
/// common case (interceptors inspecting a request) and shouldn't require
/// `&mut`.
pub trait ReflectMessageMut: ReflectMessage {
    /// Checked variant of [`set`](Self::set).
    ///
    /// The default implementation performs **no validation** — it forwards
    /// to `set` and returns `Ok(())`, so on an implementation that has not
    /// overridden it this can panic exactly where `set` would.
    /// Implementations that can validate field-descriptor membership or
    /// runtime value shape should override it and return
    /// [`ReflectError::FieldNotMember`] or
    /// [`ReflectError::WrongValueKind`] rather than mutating invalid state
    /// ([`DynamicMessage`] does both).
    ///
    /// A `Value::Message` of the field's own type but from a *different*
    /// [`DescriptorPool`](crate::DescriptorPool) is not an error:
    /// [`DynamicMessage`] adopts it into its own pool. Cross-crate reflection
    /// produces such values by construction — a generated type reflects
    /// against its defining crate's pool — so the vtable rebuild walk
    /// (`for_each_set` + `set(fd, vr.to_owned())`) depends on the adoption.
    /// Callers that want to reject values not built from their own pool must
    /// compare [`ReflectMessage::pool`] themselves; adoption is keyed on the
    /// message's full name, so a value of a *different* type is still
    /// [`ReflectError::WrongValueKind`] whatever pool it came from.
    ///
    /// # Performance
    ///
    /// Adopting a foreign message costs one wire round-trip, O(size of the
    /// subtree). Values already homed in the target pool — everything the
    /// decoder and the JSON parser produce — pass through untouched, so a
    /// rebuild pays only for the fields that actually cross a pool boundary.
    /// On the vtable path that is two round-trips for such a field rather than
    /// one, because `to_owned` has already materialized the subtree in its
    /// defining pool before this call re-homes it.
    fn try_set(
        &mut self,
        field: &FieldDescriptor,
        value: super::Value,
    ) -> Result<(), ReflectError> {
        self.set(field, value);
        Ok(())
    }

    /// Set a field's value.
    ///
    /// Setting a singular field replaces it. Setting a `List` or `Map`
    /// value replaces the whole container.
    ///
    /// # Panics
    ///
    /// May panic if `field` is not a member of this message's descriptor or
    /// `value` does not match the field kind. Use [`try_set`](Self::try_set)
    /// when membership or value shape is not already proven.
    fn set(&mut self, field: &FieldDescriptor, value: super::Value);

    /// Checked variant of [`clear`](Self::clear).
    ///
    /// The default implementation performs **no validation** — it forwards
    /// to `clear` and returns `Ok(())`, so on an implementation that has not
    /// overridden it this can panic exactly where `clear` would.
    /// Implementations that can validate field-descriptor membership should
    /// override it and return [`ReflectError::FieldNotMember`] rather than
    /// clearing a colliding field number by accident ([`DynamicMessage`]
    /// does).
    fn try_clear(&mut self, field: &FieldDescriptor) -> Result<(), ReflectError> {
        self.clear(field);
        Ok(())
    }

    /// Clear a field, returning it to its default/absent state.
    ///
    /// # Panics
    ///
    /// May panic if `field` is not a member of this message's descriptor.
    /// Use [`try_clear`](Self::try_clear) when membership is not already proven.
    fn clear(&mut self, field: &FieldDescriptor);
}

/// A clone-on-write reflective handle.
///
/// `Borrowed` is the vtable path — a fat pointer to a generated struct that
/// directly implements [`ReflectMessage`]. `Owned` is the bridge path — a
/// boxed [`DynamicMessage`] produced by encode/decode round-trip.
///
/// Boxing the `Owned` variant is load-bearing for [`ValueRef`](super::ValueRef)'s
/// size budget. The dominant variant is `Borrowed(&dyn ReflectMessage)`, a
/// 16-byte fat pointer; with the 1-byte discriminant aligned to 8 bytes,
/// `ReflectCow` is 24 bytes. `Owned(Box<DynamicMessage>)` is a thin 8-byte
/// pointer, so it doesn't increase the footprint. If `DynamicMessage`
/// (~56 bytes: an `Arc`, a `MessageIndex`, a `BTreeMap`, and an
/// `UnknownFields`) were inlined instead of boxed, `ReflectCow` would jump
/// to ~64 bytes — and since `ValueRef::Message(ReflectCow)` sets the floor
/// for `ValueRef`'s size, that would triple `ValueRef` from 32 to ~72 bytes,
/// pushing every `get()` (including hot-path scalar reads) across two cache
/// lines. The one extra heap allocation per `Owned` fires only at entry
/// points and mixed-mode boundaries, where a full encode/decode is already
/// happening — noise against that backdrop.
///
/// The `const _:` assertion in `value.rs` locks the budget in.
pub enum ReflectCow<'a> {
    /// Borrowed reflective view over the source — the vtable path.
    Borrowed(&'a dyn ReflectMessage),
    /// Owned dynamic snapshot — the bridge path.
    Owned(Box<DynamicMessage>),
}

impl core::fmt::Debug for ReflectCow<'_> {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        match self {
            Self::Borrowed(_) => write!(f, "ReflectCow::Borrowed(..)"),
            Self::Owned(d) => f.debug_tuple("ReflectCow::Owned").field(d).finish(),
        }
    }
}

impl<'a> ReflectCow<'a> {
    /// Snapshot the underlying message as a [`DynamicMessage`].
    #[must_use]
    pub fn to_dynamic(&self) -> DynamicMessage {
        match self {
            Self::Borrowed(m) => m.to_dynamic(),
            Self::Owned(d) => (**d).clone(),
        }
    }
}

impl<'a> core::ops::Deref for ReflectCow<'a> {
    type Target = dyn ReflectMessage + 'a;

    fn deref(&self) -> &Self::Target {
        match self {
            Self::Borrowed(m) => *m,
            Self::Owned(d) => &**d,
        }
    }
}

/// Codegen entry point for reflection.
///
/// Codegen emits an impl for every generated message type whenever any
/// reflection mode is enabled. The body varies by mode: bridge mode boxes a
/// [`DynamicMessage`], vtable mode borrows the struct directly. The call site
/// is always `foo.reflect()` — flipping modes requires no diff.
#[rustversion::attr(
    since(1.78),
    diagnostic::on_unimplemented(
        message = "`{Self}` does not implement `Reflectable` — no reflection is enabled for this message type",
        note = "if `{Self}` comes from another buffa-generated crate via an extern path (well-known types resolve to `buffa-types` by default), enable that crate's reflection feature, e.g. `buffa-types = {{ version = \"...\", features = [\"reflect\"] }}`",
        note = "if `{Self}` is generated in this crate, enable reflection in its `build.rs` config: `generate_reflection(true)` (vtable) or `reflect_mode(ReflectMode::Bridge)` for the smaller bridge impl — either emits `Reflectable`"
    )
)]
pub trait Reflectable {
    /// A read-only reflective handle over `self`.
    ///
    /// # Performance
    ///
    /// Which body codegen emits depends on the reflection mode:
    ///
    /// - **Bridge mode** — `reflect()` is one full encode + decode round-trip
    ///   plus a heap allocation per call, returning an owned `DynamicMessage`
    ///   snapshot. The first call also pays a one-time pool build cost (linking
    ///   the embedded `FileDescriptorSet`).
    /// - **Vtable mode** — `reflect()` borrows `self` directly
    ///   (`ReflectCow::Borrowed`), with no round-trip and no allocation; the
    ///   reflective accessors read the message's fields in place.
    ///
    /// Either way the returned handle borrows `self` (the signature ties it to
    /// `&self`), so the call site is identical between modes. Hold onto the
    /// handle for repeated reads rather than calling `reflect()` per field; for
    /// an owned snapshot that outlives `self`, use
    /// [`ReflectCow::to_dynamic`](super::ReflectCow::to_dynamic).
    ///
    /// # Panics
    ///
    /// The bridge-mode body panics if the embedded `FileDescriptorSet` is
    /// malformed or `Self::FULL_NAME` is not registered in the package pool —
    /// both indicate a codegen bug, not consumer misuse. (Vtable mode resolves
    /// the descriptor lazily on first access with the same invariant.)
    ///
    /// # Setup
    ///
    /// The `Reflectable` impl is generated by enabling
    /// `buffa_build::Config::generate_reflection(true)` (bridge) or
    /// `generate_reflection_vtable(true)` (vtable) in `build.rs`. The consuming
    /// crate must also depend on `buffa-descriptor` with its `reflect` feature
    /// and on `std`.
    #[must_use = "reflect() returns a reflective handle borrowing self; bind it before reading fields"]
    fn reflect(&self) -> ReflectCow<'_>;

    // `reflect_mut(&mut self) -> ReflectCowMut<'_>` is part of the design but
    // deferred to the MergeSink work sketched in
    // docs/investigations/reflection.md.
}