Skip to main content

buffa_codegen/
lib.rs

1//! Shared code generation logic for buffa.
2//!
3//! This crate takes protobuf descriptors (`google.protobuf.FileDescriptorProto`,
4//! decoded from binary `FileDescriptorSet` data) and emits Rust source code
5//! that uses the `buffa` runtime.
6//!
7//! It is used by:
8//! - `protoc-gen-buffa` (protoc plugin)
9//! - `buffa-build` (build.rs integration)
10//!
11//! # Architecture
12//!
13//! The code generator is intentionally decoupled from how descriptors are
14//! obtained. It receives fully-resolved `FileDescriptorProto`s and produces
15//! Rust source strings. This means:
16//!
17//! - It doesn't parse `.proto` files.
18//! - It doesn't invoke `protoc`.
19//! - It doesn't do import resolution or name linking.
20//!
21//! All of that is handled upstream (by protoc, buf, or a future parser).
22
23pub(crate) mod comments;
24pub mod context;
25pub(crate) mod defaults;
26pub(crate) mod enumeration;
27pub(crate) mod extension;
28pub(crate) mod feature_gates;
29pub use feature_gates::FeatureGateNames;
30pub(crate) mod features;
31#[doc(hidden)]
32pub use buffa_descriptor::generated;
33pub mod idents;
34pub(crate) mod impl_message;
35pub(crate) mod impl_text;
36pub(crate) mod imports;
37pub(crate) mod lazy_view;
38pub(crate) mod message;
39pub(crate) mod oneof;
40pub(crate) mod owned_view;
41pub(crate) mod reflect;
42pub(crate) mod reflect_owned;
43pub(crate) mod reflect_view;
44pub(crate) mod view;
45
46use crate::generated::descriptor::FileDescriptorProto;
47use proc_macro2::TokenStream;
48use quote::{format_ident, quote};
49
50/// Lints suppressed on generated code at module boundaries.
51///
52/// Consumed by [`generate_module_tree`], the per-package `.mod.rs`
53/// stitcher, and `buffa-build`'s `_include.rs` writer. One list keeps
54/// them in sync.
55pub const ALLOW_LINTS: &[&str] = &[
56    "non_camel_case_types",
57    "dead_code",
58    "unused_imports",
59    // Cross-proto refs within the same package are emitted through the
60    // canonical `super::super::__buffa::view::…` path even though the
61    // target lives in the same generated module — using the bare name
62    // would resolve, but the canonical path is stable when a sibling
63    // proto defines a same-named natural-path re-export.
64    "unused_qualifications",
65    "clippy::derivable_impls",
66    "clippy::match_single_binding",
67    "clippy::uninlined_format_args",
68    "clippy::doc_lazy_continuation",
69    // A user `message View { message Inner }` produces
70    // `__buffa::view::view::InnerView`; harmless but trips this lint.
71    "clippy::module_inception",
72];
73
74/// Render [`ALLOW_LINTS`] as a `#[allow(…)]` attribute token stream.
75pub fn allow_lints_attr() -> TokenStream {
76    let lints: Vec<TokenStream> = ALLOW_LINTS
77        .iter()
78        .map(|l| syn::parse_str(l).expect("lint name parses as path"))
79        .collect();
80    quote! { #[allow( #(#lints),* )] }
81}
82
83/// One generated output file.
84///
85/// Each `.proto` produces up to five **content files** (`<stem>.rs`,
86/// `<stem>.__view.rs`, `<stem>.__oneof.rs`, `<stem>.__view_oneof.rs`,
87/// `<stem>.__ext.rs`) and each proto package produces one
88/// `<dotted.pkg>.mod.rs` **stitcher** that `include!`s the content files
89/// and authors the `pub mod __buffa { … }` ancillary tree.
90/// Ancillary kinds with no content for that input file (e.g. a message
91/// with no oneofs and no extensions) are omitted, and the stitcher's
92/// `include!` set is filtered to match. The `__buffa` wrapper (and each
93/// `view` / `oneof` / `ext` submodule inside it) is itself omitted when
94/// it would be empty, so packages with only owned messages emit no
95/// `__buffa` block at all.
96/// See `DESIGN.md` → "Generated code layout".
97///
98/// Consumers normally only need to wire up the
99/// [`GeneratedFileKind::PackageMod`] entries (one per package); the
100/// per-proto content kinds are reached transitively via `include!` from
101/// the stitcher. Write all files to disk; build a module tree from only
102/// the `PackageMod` ones.
103///
104/// With [`CodeGenConfig::file_per_package`] set, the per-proto content
105/// kinds are not emitted at all — the single `<dotted.pkg>.rs` (still
106/// kind `PackageMod`) inlines what the stitcher would `include!`.
107#[derive(Debug)]
108pub struct GeneratedFile {
109    /// The output file path (e.g., `"my.pkg.foo.rs"` or `"my.pkg.mod.rs"`).
110    pub name: String,
111    /// The proto package this file belongs to.
112    pub package: String,
113    /// What this file contains. Build integrations only need to wire up
114    /// [`GeneratedFileKind::PackageMod`] files; everything else is reached
115    /// via `include!` from there.
116    pub kind: GeneratedFileKind,
117    /// The generated Rust source code.
118    pub content: String,
119}
120
121/// Kind of [`GeneratedFile`].
122///
123/// [`generate`] produces up to five per-proto content kinds — one each
124/// of [`Owned`](Self::Owned), [`View`](Self::View), [`Oneof`](Self::Oneof),
125/// [`ViewOneof`](Self::ViewOneof), and [`Ext`](Self::Ext) per input
126/// `.proto` file — plus one [`PackageMod`](Self::PackageMod) stitcher per
127/// package. Kinds with no content for the input (a proto with no oneofs
128/// emits no [`Oneof`](Self::Oneof) / [`ViewOneof`](Self::ViewOneof);
129/// no extensions, no [`Ext`](Self::Ext); etc.) are omitted. Build
130/// integrations only need to wire up `PackageMod` entries; the per-proto
131/// content kinds are reached via `include!` from the stitcher and need
132/// only be written to disk alongside it. Under
133/// [`CodeGenConfig::file_per_package`] only `PackageMod` is emitted.
134///
135/// [`Companion`](Self::Companion) is the one kind *not* produced by
136/// [`generate`]: downstream code generators construct `Companion` files
137/// themselves and merge them into buffa's output via
138/// [`apply_companions`].
139///
140/// This enum is `#[non_exhaustive]` — match with a wildcard arm so new
141/// kinds can be added without a major version bump.
142#[derive(Debug, Clone, Copy, PartialEq, Eq)]
143#[non_exhaustive]
144pub enum GeneratedFileKind {
145    /// Owned message structs and enums (`<stem>.rs`).
146    Owned,
147    /// View structs (`<stem>.__view.rs`).
148    View,
149    /// Lazy view structs (`<stem>.__lazy_view.rs`).
150    LazyView,
151    /// Owned oneof enums (`<stem>.__oneof.rs`).
152    Oneof,
153    /// View oneof enums (`<stem>.__view_oneof.rs`).
154    ViewOneof,
155    /// File-level proto-extension consts (`<stem>.__ext.rs`) — the
156    /// `pub const` `ExtensionDescriptor` items generated from `extend`
157    /// blocks. Not to be confused with [`Companion`](Self::Companion),
158    /// which is unrelated downstream-supplied content.
159    Ext,
160    /// Per-package stitcher (`<dotted.pkg>.mod.rs`). The only file build
161    /// systems need to wire up directly.
162    PackageMod,
163    /// Extra per-proto content from a downstream code generator (service
164    /// stubs, extra trait impls, etc.) that travels with buffa's output.
165    ///
166    /// Not produced by [`generate`]. Construct these in your own generator
167    /// and pass them to [`apply_companions`], which appends an `include!`
168    /// for each one at file scope in the matching package's
169    /// [`PackageMod`](Self::PackageMod) — after buffa's own output, at
170    /// package root alongside the owned message types (**not** under the
171    /// `__buffa::` sentinel module). Items declared `pub` in a companion
172    /// file are visible at `crate::<pkg>::*`.
173    ///
174    /// Not to be confused with [`Ext`](Self::Ext), which is the buffa-
175    /// generated file holding protobuf `extend` consts.
176    Companion,
177}
178
179/// Parse a custom owned-type path string (e.g. `"::smol_str::SmolStr"`) into a
180/// token stream, validating it as a Rust type so a malformed path surfaces as a
181/// codegen error rather than unparseable generated output.
182pub(crate) fn parse_custom_type_path(path: &str) -> Result<proc_macro2::TokenStream, CodeGenError> {
183    let ty: syn::Type =
184        syn::parse_str(path).map_err(|_| CodeGenError::InvalidTypePath(path.to_string()))?;
185    Ok(quote::quote! { #ty })
186}
187
188/// Parse a custom **map** container path, which is applied as `path<K, V>`.
189///
190/// The path must therefore be a bare type path with no `<...>` parameters of its
191/// own (and, unlike the box/repeated knobs, no `*` placeholder — a map's key and
192/// value are appended positionally). Reject anything else with a message that
193/// names the convention, rather than letting `Foo<Bar><K, V>` surface as an
194/// opaque whole-file parse error later.
195pub(crate) fn parse_custom_map_path(path: &str) -> Result<proc_macro2::TokenStream, CodeGenError> {
196    let ty: syn::Type = syn::parse_str(path).map_err(|_| {
197        CodeGenError::InvalidTypePath(format!(
198            "{path} (map custom path takes no `<K, V>` parameters and no `*` placeholder)"
199        ))
200    })?;
201    let syn::Type::Path(tp) = &ty else {
202        return Err(CodeGenError::InvalidTypePath(format!(
203            "{path} (map custom path must be a plain type path)"
204        )));
205    };
206    if tp
207        .path
208        .segments
209        .iter()
210        .any(|s| !matches!(s.arguments, syn::PathArguments::None))
211    {
212        return Err(CodeGenError::InvalidTypePath(format!(
213            "{path} (map custom path must not include `<K, V>`; the key and value are appended automatically)"
214        )));
215    }
216    Ok(quote::quote! { #ty })
217}
218
219/// Build a custom wrapper type from a `*`-templated path and a resolved inner
220/// type, validating the result as a Rust type.
221///
222/// `*` cannot be a parsed placeholder (it is not valid in Rust type position),
223/// so substitution is textual — every `*` in `template` is replaced by `inner`'s
224/// token text before the whole string is parsed. Used by the pluggable pointer
225/// knob, where the wrapped type sits inside extra generic parameters (e.g.
226/// `"smallbox::SmallBox<*, S4>"`). The template must contain at least one `*`.
227pub(crate) fn parse_wildcard_type_path(
228    template: &str,
229    inner: &proc_macro2::TokenStream,
230) -> Result<proc_macro2::TokenStream, CodeGenError> {
231    if !template.contains('*') {
232        return Err(CodeGenError::MissingWildcard(template.to_string()));
233    }
234    let substituted = template.replace('*', &inner.to_string());
235    let ty: syn::Type = syn::parse_str(&substituted)
236        .map_err(|_| CodeGenError::InvalidTypePath(format!("{template} (as {substituted})")))?;
237    Ok(quote::quote! { #ty })
238}
239
240/// Build a custom collection type from a `*`-templated path and the resolved
241/// element type, validating the result as a Rust type.
242///
243/// `*` cannot be a parsed placeholder (it is not valid in Rust type position),
244/// so substitution is textual — every `*` in `template` is replaced by the
245/// element's token text before the whole string is parsed. The template must
246/// contain at least one `*`, otherwise the element type would have nowhere to
247/// go and the field would silently drop its element type.
248pub(crate) fn parse_custom_list_path(
249    template: &str,
250    elem: &proc_macro2::TokenStream,
251) -> Result<proc_macro2::TokenStream, CodeGenError> {
252    if !template.contains('*') {
253        return Err(CodeGenError::MissingListPlaceholder(template.to_string()));
254    }
255    let substituted = template.replace('*', &elem.to_string());
256    let ty: syn::Type = syn::parse_str(&substituted)
257        .map_err(|_| CodeGenError::InvalidTypePath(template.to_string()))?;
258    Ok(quote::quote! { #ty })
259}
260
261/// The Rust type a proto `string` field maps to in generated owned structs.
262///
263/// The default is [`String`](StringRepr::String).
264/// [`Custom`](StringRepr::Custom) substitutes any type named by its
265/// fully-qualified Rust path — for example `::smol_str::SmolStr`,
266/// `::ecow::EcoString`, or `::compact_str::CompactString` for read-mostly
267/// schemas — that satisfies the `buffa::ProtoString` bound. The downstream crate
268/// must itself depend on the crate providing that type (buffa does not re-export
269/// it).
270///
271/// Select a representation through `buffa_build`'s `string_type` /
272/// `string_type_custom` builder methods. The wire format is identical regardless
273/// of representation — only the in-memory owned type changes; view types keep
274/// borrowing `&str`, and `map<_, string>` / `map<string, _>` keys and values
275/// always stay `String`.
276#[derive(Debug, Clone, PartialEq, Eq, Default)]
277#[non_exhaustive]
278pub enum StringRepr {
279    /// `::buffa::alloc::string::String` — growable and mutable (the default).
280    #[default]
281    String,
282    /// A custom type named by its fully-qualified Rust path (e.g.
283    /// `"::smol_str::SmolStr"`). Must satisfy `buffa::ProtoString` and be
284    /// provided by a crate the downstream depends on.
285    ///
286    /// # Limitations
287    ///
288    /// - A *foreign* custom type used as a `repeated` element fails to compile
289    ///   (the emitted `ReflectElement` impl violates the orphan rule). Wrap it
290    ///   in a crate-local newtype for that case; singular / optional / oneof /
291    ///   map uses work with a foreign type directly.
292    /// - A path that does not parse as a Rust type surfaces as
293    ///   [`CodeGenError::InvalidTypePath`] at generation (`.compile()`) time.
294    /// - The per-element impls are deduplicated within a single generation, but
295    ///   the *same* crate-local type used as a `repeated` element across two
296    ///   separate `compile()` invocations in one crate emits the impl twice (a
297    ///   duplicate-impl `E0119`). Generate from a single `compile()`, or use
298    ///   distinct element types.
299    Custom(String),
300}
301
302impl StringRepr {
303    /// The owned Rust type path emitted for a `string` field with this
304    /// representation.
305    ///
306    /// `ctx` and `nesting` route the default `String` through the package-root
307    /// import registry (`idiomatic_imports`); a custom path is parsed and
308    /// emitted fully qualified.
309    ///
310    /// # Errors
311    ///
312    /// Returns [`CodeGenError::InvalidTypePath`] if a custom path does not parse
313    /// as a Rust type.
314    pub(crate) fn type_path(
315        &self,
316        resolver: &imports::ImportResolver,
317        ctx: &context::CodeGenContext,
318        nesting: usize,
319    ) -> Result<proc_macro2::TokenStream, CodeGenError> {
320        match self {
321            StringRepr::String => Ok(resolver.string_at(ctx, nesting)),
322            StringRepr::Custom(path) => parse_custom_type_path(path),
323        }
324    }
325
326    /// Whether this is the default `String` representation, which keeps the
327    /// `String`-specialized fast paths (in-place `merge_string`, `clear()`,
328    /// native `Arbitrary`) instead of the generic `ProtoString` ones.
329    pub(crate) fn is_default(&self) -> bool {
330        matches!(self, StringRepr::String)
331    }
332}
333
334/// The Rust type a proto `bytes` field maps to in generated owned structs.
335///
336/// The default is [`Vec`](BytesRepr::Vec) (`Vec<u8>`). [`Bytes`](BytesRepr::Bytes)
337/// uses `bytes::Bytes`, which decodes zero-copy from a
338/// `Bytes`-backed buffer. [`Custom`](BytesRepr::Custom) substitutes any type
339/// named by its fully-qualified Rust path that satisfies the `buffa::ProtoBytes`
340/// bound; the downstream crate must itself depend on the providing crate.
341///
342/// Select a representation through `buffa_build`'s `bytes_type` /
343/// `bytes_type_custom` builder methods (or the legacy `use_bytes_type`, which
344/// selects [`Bytes`](BytesRepr::Bytes)). The wire format is identical regardless
345/// of representation; view types keep borrowing `&[u8]`, and `map` bytes values
346/// follow the same rules as the string path.
347#[derive(Debug, Clone, PartialEq, Eq, Default)]
348#[non_exhaustive]
349pub enum BytesRepr {
350    /// `::buffa::alloc::vec::Vec<u8>` — growable and mutable (the default).
351    #[default]
352    Vec,
353    /// `::buffa::bytes::Bytes` — reference-counted, immutable, decodes zero-copy
354    /// from a `Bytes`-backed buffer.
355    Bytes,
356    /// A custom type named by its fully-qualified Rust path. Must satisfy
357    /// `buffa::ProtoBytes` and be provided by a crate the downstream depends on.
358    ///
359    /// # Limitations
360    ///
361    /// - A *foreign* custom type used as a `repeated` element fails to compile
362    ///   (the emitted `ReflectElement` / `ProtoElemJson` impls violate the
363    ///   orphan rule). Wrap it in a crate-local newtype for that case; singular
364    ///   / optional / oneof uses work with a foreign type directly.
365    /// - A `Custom` rule does **not** apply to `map<K, bytes>` values — they
366    ///   stay `Vec<u8>`. Only the built-in [`Bytes`](BytesRepr::Bytes) applies
367    ///   to map values.
368    /// - A path that does not parse as a Rust type surfaces as
369    ///   [`CodeGenError::InvalidTypePath`] at generation (`.compile()`) time.
370    /// - The per-element impls are deduplicated within a single generation, but
371    ///   the *same* crate-local type used as a `repeated` element across two
372    ///   separate `compile()` invocations in one crate emits the impl twice (a
373    ///   duplicate-impl `E0119`). Generate from a single `compile()`, or use
374    ///   distinct element types.
375    Custom(String),
376}
377
378impl BytesRepr {
379    /// The owned Rust type path emitted for a `bytes` field with this
380    /// representation.
381    ///
382    /// `ctx` and `nesting` route the default `Vec<u8>` through the package-root
383    /// import registry; `Bytes` and a custom path are emitted fully qualified.
384    ///
385    /// # Errors
386    ///
387    /// Returns [`CodeGenError::InvalidTypePath`] if a custom path does not parse
388    /// as a Rust type.
389    pub(crate) fn type_path(
390        &self,
391        resolver: &imports::ImportResolver,
392        ctx: &context::CodeGenContext,
393        nesting: usize,
394    ) -> Result<proc_macro2::TokenStream, CodeGenError> {
395        use quote::quote;
396        match self {
397            BytesRepr::Vec => {
398                let vec = resolver.vec_at(ctx, nesting);
399                Ok(quote! { #vec<u8> })
400            }
401            BytesRepr::Bytes => Ok(quote! { ::buffa::bytes::Bytes }),
402            BytesRepr::Custom(path) => parse_custom_type_path(path),
403        }
404    }
405
406    /// Whether this is the default `Vec<u8>` representation, which keeps the
407    /// `Vec`-specialized fast paths (in-place `merge_bytes`, `clear()`, native
408    /// `Arbitrary`) instead of the generic `ProtoBytes` ones.
409    pub(crate) fn is_default(&self) -> bool {
410        matches!(self, BytesRepr::Vec)
411    }
412}
413
414/// The owned Rust collection a proto `map<K, V>` field maps to in generated
415/// owned structs.
416///
417/// The default is [`HashMap`](MapRepr::HashMap) (`std::collections::HashMap`, or
418/// `hashbrown::HashMap` under `no_std`). [`BTreeMap`](MapRepr::BTreeMap) selects
419/// the buffa-provided `alloc::collections::BTreeMap` for deterministic iteration
420/// order with no extra dependency or consumer code.
421/// [`Custom`](MapRepr::Custom) substitutes any map that satisfies the
422/// `buffa::map_codec::MapStorage` bound — for example a crate-local newtype
423/// wrapping `indexmap::IndexMap`.
424///
425/// Unlike the `repeated` knob (which wraps the element type and needs a `*`
426/// placeholder template), a map type is always `path<K, V>` with both
427/// parameters positional and buffa-resolved, so a custom path is a plain type
428/// path (e.g. `"::my_crate::OrderedMap"`) with no placeholder.
429///
430/// Select a representation through `buffa_build`'s `map_type` /
431/// `map_type_custom` builder methods. The wire format is identical regardless of
432/// the collection; only the in-memory owned type changes.
433#[derive(Debug, Clone, PartialEq, Eq, Default)]
434#[non_exhaustive]
435pub enum MapRepr {
436    /// `::buffa::__private::HashMap<K, V>` — the default. Generated output is
437    /// byte-identical to a build without the knob.
438    #[default]
439    HashMap,
440    /// `::buffa::alloc::collections::BTreeMap<K, V>` — buffa-provided, no extra
441    /// dependency, deterministic key order (so encoded bytes are stable across
442    /// runs). The key type must be `Ord`, which every proto map key type
443    /// (integers, bool, string) satisfies.
444    BTreeMap,
445    /// A custom map named by a fully-qualified Rust type path (e.g.
446    /// `"::my_crate::OrderedMap"`). The named type must satisfy
447    /// `buffa::map_codec::MapStorage` and be a **crate-local newtype** (a foreign
448    /// map cannot implement the buffa-owned reflection / serde traits).
449    ///
450    /// # Limitations
451    ///
452    /// - The path is a plain type path applied as `path<K, V>` — it must **not**
453    ///   include the `<K, V>` parameters or a `*` placeholder. A path that does
454    ///   not parse as a Rust type surfaces as [`CodeGenError::InvalidTypePath`]
455    ///   at generation (`.compile()`) time.
456    /// - The newtype must implement `buffa::map_codec::MapStorage` plus the
457    ///   derive / `FromIterator` / `ReflectMap` / serde / `arbitrary` bounds
458    ///   listed on that trait's docs (the canonical list). JSON and `arbitrary`
459    ///   now work for every proto map key/value type regardless of the container.
460    ///   The buffa-provided [`BTreeMap`](MapRepr::BTreeMap) already satisfies every
461    ///   bound, so prefer it unless you need a specific foreign map.
462    Custom(String),
463}
464
465impl MapRepr {
466    /// The owned Rust map type emitted for a `map<K, V>` field with this
467    /// representation, given the already-resolved key and value type tokens.
468    ///
469    /// `ctx` and `nesting` route the default `HashMap` through the package-root
470    /// import registry; `BTreeMap` and a custom path are emitted fully
471    /// qualified.
472    ///
473    /// # Errors
474    ///
475    /// Returns [`CodeGenError::InvalidTypePath`] if a custom path does not parse
476    /// as a Rust type.
477    pub(crate) fn type_path(
478        &self,
479        key: &proc_macro2::TokenStream,
480        value: &proc_macro2::TokenStream,
481        resolver: &imports::ImportResolver,
482        ctx: &context::CodeGenContext,
483        nesting: usize,
484    ) -> Result<proc_macro2::TokenStream, CodeGenError> {
485        use quote::quote;
486        match self {
487            MapRepr::HashMap => {
488                let hm = resolver.hashmap_at(ctx, nesting);
489                Ok(quote! { #hm<#key, #value> })
490            }
491            MapRepr::BTreeMap => Ok(quote! { ::buffa::alloc::collections::BTreeMap<#key, #value> }),
492            MapRepr::Custom(path) => {
493                let ty = parse_custom_map_path(path)?;
494                Ok(quote! { #ty<#key, #value> })
495            }
496        }
497    }
498
499    /// Whether this is the default `HashMap` representation, whose generated
500    /// output is byte-identical to a build without the knob.
501    pub(crate) fn is_default(&self) -> bool {
502        matches!(self, MapRepr::HashMap)
503    }
504}
505
506/// The owned smart pointer a singular message field's `buffa::MessageField`
507/// wraps in generated owned structs.
508///
509/// The default is [`Box`](PointerRepr::Box). [`Custom`](PointerRepr::Custom)
510/// substitutes any pointer that satisfies the `buffa::ProtoBox<T>` bound — for
511/// example a `smallbox`-style pointer that stores small messages inline.
512/// Because the pointer *wraps* the message type, its path is a **template**
513/// containing a `*` placeholder for the message type (e.g.
514/// `"::smallbox::SmallBox<*, ::smallbox::space::S4>"` or
515/// `"::my_crate::SmallBox<*>"`).
516///
517/// Because `buffa::ProtoBox` is buffa-owned, a *foreign* pointer cannot
518/// implement it directly (orphan rule) — the template must name a crate-local
519/// newtype, mirroring the `ProtoString` newtype expectation.
520///
521/// Select a representation through `buffa_build`'s `box_type_custom` builder
522/// method. The wire format is identical regardless of the pointer; view types
523/// are unaffected. Applies to singular message fields and **boxed** oneof
524/// message/group variants (a variant opted into inline storage via
525/// `unboxed_oneof_fields` takes precedence and gets no pointer). Repeated
526/// message fields use a collection, not a pointer.
527#[derive(Debug, Clone, PartialEq, Eq, Default)]
528#[non_exhaustive]
529pub enum PointerRepr {
530    /// `::buffa::alloc::boxed::Box<T>` (inside `MessageField<T>`) — the default.
531    /// Keeps generated output byte-identical to a build without the knob (the
532    /// `MessageField` pointer type parameter defaults to `Box`).
533    #[default]
534    Box,
535    /// A custom pointer named by a Rust type-path **template** with a `*`
536    /// placeholder for the message type. Must satisfy `buffa::ProtoBox<T>` and
537    /// be a crate-local newtype.
538    ///
539    /// # Limitations
540    ///
541    /// - The template must contain at least one `*`; a template that omits it
542    ///   surfaces as [`CodeGenError::MissingWildcard`], and one whose
543    ///   substitution does not parse as [`CodeGenError::InvalidTypePath`], at
544    ///   generation (`.compile()`) time.
545    /// - `Rc` / `Arc` and other shared/COW pointers are unusable: the decoder
546    ///   merges in place (needs `DerefMut`), so only an exclusively-owned
547    ///   pointer (heap `Box`, inline `SmallBox`) can implement `ProtoBox`.
548    /// - An inline pointer inflates the parent struct per field, so select it
549    ///   per field/prefix, never as a blanket default.
550    /// - On a **boxed oneof variant** under the `arbitrary` feature, the custom
551    ///   pointer must implement `arbitrary::Arbitrary` (the oneof enum derives it
552    ///   and stores the pointer directly in the variant). The singular-field path
553    ///   needs no such impl — `MessageField` constructs the pointer itself.
554    Custom(String),
555}
556
557impl PointerRepr {
558    /// The owned `MessageField<...>` type emitted for a singular message field
559    /// with this representation, given the resolved inner message type tokens
560    /// and the `MessageField` path from the resolver.
561    ///
562    /// # Errors
563    ///
564    /// Returns [`CodeGenError::MissingWildcard`] if a custom template omits `*`,
565    /// or [`CodeGenError::InvalidTypePath`] if it does not parse once the message
566    /// type is substituted.
567    pub(crate) fn type_path(
568        &self,
569        message_field: &proc_macro2::TokenStream,
570        inner: &proc_macro2::TokenStream,
571    ) -> Result<proc_macro2::TokenStream, CodeGenError> {
572        use quote::quote;
573        match self {
574            PointerRepr::Box => Ok(quote! { #message_field<#inner> }),
575            PointerRepr::Custom(template) => {
576                let ptr = parse_wildcard_type_path(template, inner)?;
577                Ok(quote! { #message_field<#inner, #ptr> })
578            }
579        }
580    }
581
582    /// The fully-qualified `::buffa::MessageField::<...>` path for a
583    /// `::some(value)` construction of a singular message field with this
584    /// representation: `<inner>` for `Box` (the pointer param defaults), or
585    /// `<inner, ptr>` for a custom pointer. The view→owned conversion uses this
586    /// so the constructed `MessageField` matches the field's declared type.
587    ///
588    /// # Errors
589    ///
590    /// As [`type_path`](Self::type_path) for a custom template.
591    pub(crate) fn some_path(
592        &self,
593        inner: &proc_macro2::TokenStream,
594    ) -> Result<proc_macro2::TokenStream, CodeGenError> {
595        use quote::quote;
596        match self {
597            PointerRepr::Box => Ok(quote! { ::buffa::MessageField::<#inner> }),
598            PointerRepr::Custom(template) => {
599                let ptr = parse_wildcard_type_path(template, inner)?;
600                Ok(quote! { ::buffa::MessageField::<#inner, #ptr> })
601            }
602        }
603    }
604
605    /// The bare pointer type wrapping `inner` for a **boxed oneof variant**
606    /// (`Box<inner>` by default, or the custom pointer). Unlike
607    /// [`type_path`](Self::type_path) this is the pointer alone, not wrapped in
608    /// `MessageField`, because a oneof enum stores the pointer directly in the
609    /// variant.
610    ///
611    /// # Errors
612    ///
613    /// As [`type_path`](Self::type_path) for a custom template.
614    pub(crate) fn pointer_type(
615        &self,
616        inner: &proc_macro2::TokenStream,
617    ) -> Result<proc_macro2::TokenStream, CodeGenError> {
618        use quote::quote;
619        match self {
620            PointerRepr::Box => Ok(quote! { ::buffa::alloc::boxed::Box<#inner> }),
621            PointerRepr::Custom(template) => parse_wildcard_type_path(template, inner),
622        }
623    }
624
625    /// Construct the pointer from a value expression for a boxed oneof variant:
626    /// `Box::new(value)` (byte-identical default) or the fully-qualified
627    /// `<Ptr as ProtoBox<inner>>::new(value)` for a custom pointer (so an
628    /// inherent `new` on the pointer can't shadow the trait method).
629    ///
630    /// # Errors
631    ///
632    /// As [`type_path`](Self::type_path) for a custom template.
633    pub(crate) fn pointer_new(
634        &self,
635        inner: &proc_macro2::TokenStream,
636        value: &proc_macro2::TokenStream,
637    ) -> Result<proc_macro2::TokenStream, CodeGenError> {
638        use quote::quote;
639        match self {
640            PointerRepr::Box => Ok(quote! { ::buffa::alloc::boxed::Box::new(#value) }),
641            PointerRepr::Custom(template) => {
642                let ptr = parse_wildcard_type_path(template, inner)?;
643                Ok(quote! { <#ptr as ::buffa::ProtoBox<#inner>>::new(#value) })
644            }
645        }
646    }
647}
648
649/// The owned Rust collection a proto `repeated` field maps to in generated
650/// owned structs.
651///
652/// The default is [`Vec`](RepeatedRepr::Vec) (`Vec<T>`).
653/// [`Custom`](RepeatedRepr::Custom) substitutes any collection that satisfies
654/// the `buffa::ProtoList<T>` bound — for example a crate-local newtype wrapping
655/// a `SmallVec`-backed inline collection. Unlike the scalar `string`/`bytes`
656/// knobs the custom collection *wraps* the element type, so its path is a
657/// **template** containing a `*` placeholder where the element type is
658/// substituted (e.g. `"::my_crate::SmallList<*>"`).
659///
660/// Because `buffa::ProtoList` is buffa-owned, a *foreign* collection cannot
661/// implement it directly (orphan rule) — the template must always name a
662/// crate-local newtype, mirroring the `ProtoString` newtype expectation.
663///
664/// Select a representation through `buffa_build`'s `repeated_type_custom`
665/// builder method. The wire format is identical regardless of the collection;
666/// view types keep borrowing `&[T]`.
667#[derive(Debug, Clone, PartialEq, Eq, Default)]
668#[non_exhaustive]
669pub enum RepeatedRepr {
670    /// `::buffa::alloc::vec::Vec<T>` — the default. Keeps the `Vec`-specialized
671    /// fast paths (in-place `push`/`reserve`/`clear`, native `Arbitrary`)
672    /// instead of the generic `ProtoList` ones, so generated output for the
673    /// default is byte-identical to a build without the knob.
674    #[default]
675    Vec,
676    /// A custom collection named by a Rust type-path **template** with a `*`
677    /// placeholder for the element type (e.g. `"::my_crate::SmallList<*>"`). The
678    /// named type must satisfy `buffa::ProtoList<T>` and be a **crate-local
679    /// newtype** (a foreign collection cannot implement the buffa-owned
680    /// `ProtoList`).
681    ///
682    /// # Limitations
683    ///
684    /// - The template must contain at least one `*`; the element type is
685    ///   substituted for every `*` before the result is parsed as a Rust type.
686    ///   A template that omits `*` surfaces as
687    ///   [`CodeGenError::MissingListPlaceholder`], and one whose substitution
688    ///   does not parse as [`CodeGenError::InvalidTypePath`], at generation
689    ///   (`.compile()`) time.
690    /// - A custom collection always needs a crate-local newtype — this is not
691    ///   limited to the reflection path. The generated decode and clear code
692    ///   require `Field: ProtoList`, so even a binary-only build cannot use a
693    ///   foreign collection directly.
694    /// - Under reflection / vtable the newtype must implement
695    ///   `buffa_descriptor`'s `ReflectList` (a `Vec`-backed newtype can delegate
696    ///   to the inner `Vec<T>: ReflectList`). Under JSON it must implement
697    ///   `serde::Serialize` / `Deserialize`; under the `arbitrary` feature,
698    ///   `arbitrary::Arbitrary` (derivable on a newtype).
699    /// - A `repeated <self-type>` field becomes `Collection<Self>`, so the
700    ///   collection must be heap-backed; an inline collection (`SmallVec<[Self;
701    ///   N]>`) would be infinitely sized and fail to compile.
702    Custom(String),
703}
704
705impl RepeatedRepr {
706    /// The owned Rust collection type emitted for a `repeated` field with this
707    /// representation, given the already-resolved element type tokens.
708    ///
709    /// `ctx` and `nesting` route the default `Vec` through the package-root
710    /// import registry; a custom template has its `*` placeholders replaced by
711    /// `elem` and the result is parsed and emitted fully qualified.
712    ///
713    /// # Errors
714    ///
715    /// Returns [`CodeGenError::MissingListPlaceholder`] if a custom template
716    /// omits `*`, or [`CodeGenError::InvalidTypePath`] if it does not parse as a
717    /// Rust type once the element is substituted.
718    pub(crate) fn type_path(
719        &self,
720        elem: &proc_macro2::TokenStream,
721        resolver: &imports::ImportResolver,
722        ctx: &context::CodeGenContext,
723        nesting: usize,
724    ) -> Result<proc_macro2::TokenStream, CodeGenError> {
725        use quote::quote;
726        match self {
727            RepeatedRepr::Vec => {
728                let vec = resolver.vec_at(ctx, nesting);
729                Ok(quote! { #vec<#elem> })
730            }
731            RepeatedRepr::Custom(template) => parse_custom_list_path(template, elem),
732        }
733    }
734
735    /// Whether this is the default `Vec` representation, which keeps the
736    /// `Vec`-specialized fast paths instead of the generic `ProtoList` ones.
737    pub(crate) fn is_default(&self) -> bool {
738        matches!(self, RepeatedRepr::Vec)
739    }
740}
741
742/// How much reflection support generated types get.
743///
744/// Selected through `buffa_build`'s `reflect_mode` builder method (or the
745/// `protoc-gen-buffa` `reflect_mode=` option). All modes need the consuming
746/// crate to depend on `buffa-descriptor` with its `reflect` feature and on
747/// `std`; the call site is `foo.reflect().get(fd)` regardless of mode.
748#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
749#[non_exhaustive]
750pub enum ReflectMode {
751    /// No reflection impls.
752    #[default]
753    Off,
754    /// `Reflectable::reflect()` round-trips the message through a
755    /// `DynamicMessage` (encode → decode → boxed handle). Smaller generated
756    /// code; pays an allocation and a re-encode per `reflect()` call.
757    Bridge,
758    /// `impl ReflectMessage` directly on the owned and view types, and
759    /// `Reflectable::reflect()` borrows `self` with no round-trip. Larger
760    /// generated code; near-free reflective access. Does not require view
761    /// generation — with views off, only the owned impls are emitted.
762    VTable,
763}
764
765impl ReflectMode {
766    /// Apply this mode to a [`CodeGenConfig`] (sets `generate_reflection` /
767    /// `generate_reflection_vtable`). Used by the `buffa-build` and
768    /// `protoc-gen-buffa` front-ends.
769    pub fn apply(self, config: &mut CodeGenConfig) {
770        let (reflection, vtable) = match self {
771            ReflectMode::Off => (false, false),
772            ReflectMode::Bridge => (true, false),
773            ReflectMode::VTable => (true, true),
774        };
775        config.generate_reflection = reflection;
776        config.generate_reflection_vtable = vtable;
777    }
778}
779
780/// Configuration for code generation.
781#[derive(Debug, Clone)]
782#[non_exhaustive]
783pub struct CodeGenConfig {
784    /// Whether to generate borrowed view types (`MyMessageView<'a>`) in
785    /// addition to owned types.
786    pub generate_views: bool,
787    /// Whether to additionally generate the lazy view family
788    /// (`MyMessageLazyView<'a>`) alongside the eager views (default: false).
789    ///
790    /// Lazy views implement `buffa::LazyMessageView`: `decode_lazy` performs
791    /// a single non-recursive scan, recording singular/repeated message
792    /// fields as undecoded byte ranges (`LazyMessageFieldView` /
793    /// `LazyRepeatedView`) that decode on access — reading a few fields of
794    /// many sub-messages no longer allocates or recurses into untouched
795    /// sub-trees. The eager `MyMessageView` family is unchanged (output is
796    /// byte-identical with or without this flag), so eager and lazy views
797    /// coexist and generic `MessageView` consumers never silently inherit
798    /// deferred validation.
799    ///
800    /// Semantics of the lazy family:
801    ///
802    /// - **Eager carve-outs**: groups / editions `DELIMITED` fields (no
803    ///   length prefix to defer), oneof message variants, and map message
804    ///   values use the eager view types.
805    /// - **Merge preserved**: a singular message field split across wire
806    ///   occurrences is recorded as fragments and merged on access.
807    /// - **Budgets flow**: the recursion depth and unknown-field allowance
808    ///   remaining at each deferred field are recorded and replayed per
809    ///   access (a per-subtree approximation of the shared allowance).
810    /// - **Deferred validation**: malformed deferred bytes error on access,
811    ///   from the fallible `to_owned_message`, and as a serde error from the
812    ///   view `Serialize` impl. `ViewEncode` replays recorded fragments
813    ///   **without validating them**.
814    /// - No `ReflectMessage`, `OwnedView`, or text-format surface — use the
815    ///   eager family for those.
816    ///
817    /// Requires [`generate_views`](Self::generate_views) (the lazy family
818    /// reuses the eager view-oneof enums and eager sub-view types); with
819    /// views disabled the flag is ignored with a warning.
820    pub lazy_views: bool,
821    /// Whether to preserve unknown fields (default: true).
822    pub preserve_unknown_fields: bool,
823    /// Whether to derive `serde::Serialize` / `serde::Deserialize` on
824    /// generated message structs and enum types, and emit `#[serde(with = "...")]`
825    /// attributes for proto3 JSON's special scalar encodings (int64 as quoted
826    /// string, bytes as base64, etc.).
827    ///
828    /// When this is `true`, the downstream crate must depend on `serde` and
829    /// must enable the `buffa/json` feature for the runtime helpers.
830    ///
831    /// Oneof fields use `#[serde(flatten)]` with custom `Serialize` /
832    /// `Deserialize` impls so that each variant appears as a top-level
833    /// JSON field (proto3 JSON inline oneof encoding).
834    pub generate_json: bool,
835    /// Whether to emit `#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]`
836    /// on generated message structs and enum types.
837    ///
838    /// When this is `true`, the downstream crate must add `arbitrary` as an
839    /// optional dependency and enable the `buffa/arbitrary` feature. The
840    /// downstream crate's Cargo feature that gates `arbitrary` must be named
841    /// exactly `"arbitrary"` — the generated `cfg_attr` uses that literal
842    /// string and cannot be customized. This applies to both the struct-level
843    /// `derive(Arbitrary)` and the per-field `#[arbitrary(with = ...)]`
844    /// attributes emitted for `bytes_fields`-typed fields.
845    ///
846    /// For `bytes_fields`-typed fields, codegen emits `#[arbitrary(with = ...)]`
847    /// using helpers in `::buffa::__private` since `bytes::Bytes` has no
848    /// `Arbitrary` impl. Singular, optional, and repeated bytes fields are all
849    /// covered. Map values are always `Vec<u8>` regardless of `bytes_fields`
850    /// and require no special handling.
851    pub generate_arbitrary: bool,
852    /// External type path mappings.
853    ///
854    /// Each entry maps either a fully-qualified protobuf package prefix
855    /// (e.g., `".my.common"`) to a Rust module path (e.g.,
856    /// `"::common_protos"`), or a single type FQN (e.g.,
857    /// `".my.common.Shared"`) to a full Rust type path (e.g.,
858    /// `"::shared_types::Shared"`). Matched types reference the extern Rust
859    /// path instead of being generated, allowing shared proto packages to be
860    /// compiled once in a dedicated crate and referenced from others. An
861    /// exact type-FQN entry wins over a covering package prefix; otherwise
862    /// the longest matching prefix wins.
863    ///
864    /// Well-known types (`google.protobuf.*`) are automatically mapped to
865    /// `::buffa_types::google::protobuf::*` without needing an explicit
866    /// entry here. To override with a custom implementation, add an
867    /// `extern_path` for `.google.protobuf` pointing to your crate.
868    pub extern_paths: Vec<(String, String)>,
869    /// Ordered (proto-path-prefix, [`BytesRepr`]) rules selecting the Rust type
870    /// for `bytes` fields. Later rules win, so a broad rule (e.g. `"."` →
871    /// `Bytes`) can be refined by a more specific one. Fields matching no rule
872    /// use `Vec<u8>`. The path is matched with the same proto-segment-aware
873    /// prefix logic as [`string_fields`](Self::string_fields).
874    pub bytes_fields: Vec<(String, BytesRepr)>,
875    /// Ordered (proto-path-prefix, [`StringRepr`]) rules selecting the Rust type
876    /// for `string` fields. Later rules win, so a broad rule (e.g. `"."` →
877    /// `SmolStr`) can be refined by a more specific one
878    /// (`".my.pkg.Msg.field"` → `CompactString`). Fields matching no rule use
879    /// `String`. The path is matched with the same proto-segment-aware prefix
880    /// logic as [`bytes_fields`](Self::bytes_fields).
881    ///
882    /// Applies to singular, optional, and repeated `string` fields and oneof
883    /// `string` variants. Map keys and values always stay `String`, mirroring
884    /// the bytes path (where map values always stay `Vec<u8>`).
885    pub string_fields: Vec<(String, StringRepr)>,
886    /// Ordered (proto-path-prefix, [`MapRepr`]) rules selecting the owned Rust
887    /// map collection for `map` fields. Later rules win, with the same
888    /// proto-segment-aware prefix matching as [`bytes_fields`](Self::bytes_fields)
889    /// (`"."` matches every field). Fields matching no rule use `HashMap<K, V>`.
890    ///
891    /// Independent of the element/value representation: a `map` field's key and
892    /// value types are chosen by the usual scalar/string/bytes/message rules,
893    /// and this knob only changes the surrounding collection.
894    pub map_fields: Vec<(String, MapRepr)>,
895    /// Ordered (proto-path-prefix, [`PointerRepr`]) rules selecting the owned
896    /// smart pointer for singular message fields (the pointer inside
897    /// `MessageField<T>`). Later rules win, same proto-segment-aware prefix
898    /// matching as [`bytes_fields`](Self::bytes_fields). Fields matching no rule
899    /// use `Box<T>`.
900    ///
901    /// Applies to singular (and proto2 optional/required) message fields only —
902    /// not repeated message fields (a collection) or oneof message variants.
903    pub pointer_fields: Vec<(String, PointerRepr)>,
904    /// Ordered (proto-path-prefix, [`RepeatedRepr`]) rules selecting the owned
905    /// Rust collection for `repeated` fields. Later rules win, with the same
906    /// proto-segment-aware prefix matching as [`bytes_fields`](Self::bytes_fields)
907    /// (`"."` matches every field). Fields matching no rule use `Vec<T>`.
908    ///
909    /// Applies only to `repeated` fields (not `map`, whose collection stays
910    /// the configured map type). The element type is chosen by the usual
911    /// scalar/string/bytes/message rules and substituted into the collection
912    /// template.
913    pub repeated_fields: Vec<(String, RepeatedRepr)>,
914    /// Fully-qualified proto paths whose message-typed oneof variants should
915    /// **not** be wrapped in `Box<T>`. By default every message/group oneof
916    /// variant is boxed (so recursive types compile); entries here opt matching
917    /// variants out, storing the message inline in the enum.
918    ///
919    /// Each entry is a proto path prefix matched with the same
920    /// proto-segment-aware logic as [`bytes_fields`](Self::bytes_fields)
921    /// (`"."` matches every variant). Recursive variants cannot be stored
922    /// inline (the type would be unsized): an entry naming one *exactly* is
923    /// rejected at codegen time, while a broader prefix entry silently keeps
924    /// recursive variants boxed and inlines the rest.
925    pub unboxed_oneof_fields: Vec<String>,
926    /// Honor `features.utf8_validation = NONE` by emitting `Vec<u8>` / `&[u8]`
927    /// for such string fields instead of `String` / `&str`.
928    ///
929    /// When `false` (the default), buffa emits `String` for all string fields
930    /// and **validates UTF-8 on decode** — stricter than proto2 requires, but
931    /// ergonomic and safe.
932    ///
933    /// When `true`, string fields with `utf8_validation = NONE` (all proto2
934    /// strings by default, and editions fields that opt into `NONE`) become
935    /// `Vec<u8>` / `&[u8]`. Decode skips validation; the caller decides at the
936    /// call site whether to `std::str::from_utf8` (checked) or
937    /// `from_utf8_unchecked` (trusted-input fast path). This is the only
938    /// sound Rust mapping when strings may actually contain non-UTF-8 bytes.
939    ///
940    /// **This is a breaking change for proto2** — enable only for new code or
941    /// when profiling identifies UTF-8 validation as a bottleneck.
942    pub strict_utf8_mapping: bool,
943    /// Permit `option message_set_wire_format = true` on input messages.
944    ///
945    /// MessageSet is a legacy Google-internal wire format that wraps each
946    /// extension in a group structure instead of using regular field tags.
947    /// When `false` (the default), encountering such a message is a codegen
948    /// error — the flag exists to make MessageSet use explicit, since the
949    /// format is obsolete outside of interop with very old Google protos.
950    pub allow_message_set: bool,
951    /// Whether to emit `impl buffa::text::TextFormat` on generated message
952    /// structs for textproto (human-readable text format) encoding/decoding.
953    ///
954    /// When this is `true`, the downstream crate must enable the `buffa/text`
955    /// feature for the runtime encoder/decoder.
956    pub generate_text: bool,
957    /// Whether the per-package `.mod.rs` stitcher emits
958    /// `__buffa::register_types(&mut TypeRegistry)`.
959    ///
960    /// Default `true`. The fn aggregates `Any` type entries and extension
961    /// entries for every message in the package. Set to `false` for
962    /// crates that don't use extensions/`Any`, or that hand-roll
963    /// registration (e.g. `buffa-types`' `register_wkt_types`, which
964    /// knows the JSON-Any `is_wkt` special-casing the generic fn does
965    /// not). The per-message `__*_JSON_ANY` / `__*_TEXT_ANY` consts are
966    /// still emitted; only the aggregating fn is suppressed.
967    pub emit_register_fn: bool,
968    /// Emit one `<dotted.package>.rs` per proto package instead of the
969    /// per-proto-file content set plus `<pkg>.mod.rs` stitcher.
970    ///
971    /// The single file inlines what the stitcher would otherwise `include!`,
972    /// producing the same `__buffa::{view,oneof,ext,...}` module structure.
973    /// Intended for Buf Schema Registry generated SDKs, whose `lib.rs`
974    /// synthesis builds the module tree from `<dotted.package>.rs` filenames.
975    ///
976    /// Under `strategy: directory` this only sees one directory's files per
977    /// invocation, so the input module must be `PACKAGE_DIRECTORY_MATCH`-clean
978    /// (one package per directory) for the output to be complete. BSR-hosted
979    /// modules satisfy this by lint default. If a package spans multiple
980    /// directories, separate invocations each emit their own `<pkg>.rs` and
981    /// the last write wins — silent partial output, not a codegen error.
982    pub file_per_package: bool,
983    /// Custom attributes to inject on generated types (messages, enums, and
984    /// oneof enums — the latter matched on the oneof's own path,
985    /// `.my.pkg.MyMessage.my_oneof`).
986    ///
987    /// Each entry is `(proto_path, attribute)`. The `proto_path` is matched
988    /// as a prefix against the fully-qualified proto name: `"."` applies to
989    /// all types, `".my.pkg"` to types in that package, `".my.pkg.MyMessage"`
990    /// to a specific type. The `attribute` is a raw Rust attribute string
991    /// (e.g., `"#[derive(serde::Serialize)]"`).
992    pub type_attributes: Vec<(String, String)>,
993    /// Custom attributes to inject on generated struct fields.
994    ///
995    /// Each entry is `(proto_path, attribute)`. The `proto_path` is matched
996    /// as a prefix against the fully-qualified field path (e.g.,
997    /// `".my.pkg.MyMessage.my_field"`). `"."` applies to all fields.
998    pub field_attributes: Vec<(String, String)>,
999    /// Custom attributes to inject on generated message structs only (not enums).
1000    ///
1001    /// Same path-matching semantics as `type_attributes`, but only applied to
1002    /// message structs, not enum types. Useful for struct-only attributes like
1003    /// `#[serde(default)]`.
1004    pub message_attributes: Vec<(String, String)>,
1005    /// Custom attributes to inject on generated enum types only (not messages).
1006    ///
1007    /// Same path-matching semantics as `type_attributes`, but only applied to
1008    /// enum types. Useful for enum-only attributes like
1009    /// `#[derive(strum::EnumIter)]` when the user does not want to apply the
1010    /// same attribute to every message in the matched scope.
1011    pub enum_attributes: Vec<(String, String)>,
1012    /// Custom attributes to inject on generated oneof enums only (not messages,
1013    /// not regular enums).
1014    ///
1015    /// Same path-matching semantics as `type_attributes`, matched against the
1016    /// oneof's fully-qualified path (`.pkg.Message.oneof_name`). Useful when a
1017    /// oneof needs a different attribute set than the surrounding types — e.g.
1018    /// keeping `#[derive(serde::Serialize)]` on messages and oneofs while a
1019    /// separate `enum_attributes` entry puts a different serde derive on the
1020    /// regular enums.
1021    pub oneof_attributes: Vec<(String, String)>,
1022    /// Wrap generated `impl`s in `#[cfg(feature = "...")]` instead of
1023    /// emitting them unconditionally.
1024    ///
1025    /// When `true`, the impls controlled by [`generate_json`],
1026    /// [`generate_views`], and [`generate_text`] are emitted wrapped in
1027    /// `#[cfg(feature = "json" | "views" | "text")]` (or
1028    /// `#[cfg_attr(feature = ..., ...)]` for derives and field attributes)
1029    /// rather than unconditionally. The consuming crate must define matching
1030    /// Cargo features that enable the corresponding runtime support, e.g.:
1031    ///
1032    /// ```toml
1033    /// [features]
1034    /// json  = ["buffa/json", "dep:serde", "dep:serde_json"]
1035    /// views = []
1036    /// text  = ["buffa/text"]
1037    /// ```
1038    ///
1039    /// The [`generate_*`] flags still control *whether* an impl kind is
1040    /// emitted at all — this flag only controls whether it is `cfg`-gated.
1041    /// `generate_arbitrary` is always `cfg_attr`-gated on
1042    /// `feature = "arbitrary"` regardless of this flag, because `arbitrary`
1043    /// is an optional dependency by design.
1044    ///
1045    /// When [`generate_reflection`](Self::generate_reflection) is also on, the
1046    /// reflection impls are gated on `feature = "reflect"` alongside
1047    /// json/views/text. To gate *only* reflection without gating json/views/text,
1048    /// use [`gate_reflect_on_crate_feature`](Self::gate_reflect_on_crate_feature)
1049    /// instead.
1050    ///
1051    /// This is the mechanism that lets `buffa-descriptor` and `buffa-types`
1052    /// ship every impl while keeping the codegen toolchain
1053    /// (`buffa-codegen`/`buffa-build`/`protoc-gen-buffa`) lean: those crates
1054    /// depend on `buffa-descriptor` with `default-features = false` and so
1055    /// don't pull `serde`/`serde_json`/`base64`. Most consumers don't need
1056    /// this — they decide at build-script time whether to generate JSON, and
1057    /// if they say yes, they want `impl Serialize` to just exist.
1058    ///
1059    /// [`generate_json`]: Self::generate_json
1060    /// [`generate_views`]: Self::generate_views
1061    /// [`generate_text`]: Self::generate_text
1062    /// [`generate_*`]: Self::generate_json
1063    pub gate_impls_on_crate_features: bool,
1064    /// Generate `with_*` builder-style setter methods for explicit-presence fields.
1065    ///
1066    /// Each explicit-presence scalar, bytes, or enum field gets a
1067    /// `pub fn with_<name>(mut self, value: T) -> Self` method that wraps the
1068    /// value in `Some` and returns `self`, enabling chained construction:
1069    ///
1070    /// ```ignore
1071    /// let req = MyRequest::default()
1072    ///     .with_name("alice")
1073    ///     .with_timeout_ms(30_000);
1074    /// ```
1075    ///
1076    /// **Fields that receive a setter:** proto3 `optional`, proto2 `optional`,
1077    /// and editions fields with `field_presence = EXPLICIT`.
1078    ///
1079    /// **Fields that do not receive a setter:** message fields
1080    /// (`MessageField<T>`), repeated fields, map fields, oneof variant fields,
1081    /// proto2 `required` fields, and any implicit-presence field.
1082    ///
1083    /// There is no `clear_<name>` companion — to clear a field, assign `None`
1084    /// directly: `msg.name = None;`.
1085    ///
1086    /// Defaults to `true`.
1087    pub generate_with_setters: bool,
1088    /// Generate `impl Reflectable` for owned message types (bridge mode).
1089    ///
1090    /// When enabled, each generated message gets an
1091    /// `impl ::buffa_descriptor::reflect::Reflectable` whose `reflect()`
1092    /// round-trips through `DynamicMessage` (encode → decode → reflective
1093    /// handle), and the package's `__buffa::reflect` submodule embeds the
1094    /// `FileDescriptorSet` bytes plus a lazily-built `DescriptorPool`.
1095    ///
1096    /// **Runtime requirements** — the consuming crate must depend on:
1097    /// - `buffa-descriptor` with the `reflect` feature.
1098    /// - `std` (the lazy pool accessor uses `std::sync::OnceLock`).
1099    ///
1100    /// When [`gate_impls_on_crate_features`](Self::gate_impls_on_crate_features)
1101    /// is on, the impls are wrapped in `#[cfg(feature = "reflect")]` so the
1102    /// consuming crate can opt out per build.
1103    ///
1104    /// **Performance** — `reflect()` is one full encode/decode round-trip
1105    /// plus a heap allocation. The first call also pays a one-time pool
1106    /// build cost (linking the embedded `FileDescriptorSet`). For zero-copy
1107    /// reflective access over view types without the round-trip, additionally
1108    /// enable [`generate_reflection_vtable`](Self::generate_reflection_vtable).
1109    ///
1110    /// **Binary size** — each package embeds its own copy of the full
1111    /// `FileDescriptorSet` (transitive closure). For a multi-package
1112    /// codegen run this duplicates the FDS bytes per package. Acceptable
1113    /// for the bridge prototype; deduplication via a crate-root module is
1114    /// a planned follow-up.
1115    ///
1116    /// Defaults to `false`.
1117    pub generate_reflection: bool,
1118    /// Emit vtable-mode reflection: `impl ReflectMessage` / `impl
1119    /// ReflectElement` on the owned message structs and (when views are
1120    /// generated) the view types, and switch the owned
1121    /// `Reflectable::reflect()` body to borrow `self`
1122    /// (`ReflectCow::Borrowed(self)`) instead of the bridge round-trip.
1123    ///
1124    /// Reflective access then reads struct fields in place — no encode/decode
1125    /// round-trip and no per-field allocation — for both a decoded view and an
1126    /// in-memory owned message.
1127    ///
1128    /// Requires [`generate_reflection`](Self::generate_reflection) (the impls
1129    /// resolve against the same embedded `DescriptorPool`) but not
1130    /// [`generate_views`](Self::generate_views) — with views off, only the
1131    /// owned impls are emitted. Set via [`ReflectMode::VTable`]
1132    /// — front-ends expose it as `buffa_build::Config::reflect_mode` /
1133    /// `protoc-gen-buffa`'s `reflect_mode=vtable`.
1134    ///
1135    /// Defaults to `false`.
1136    pub generate_reflection_vtable: bool,
1137    /// Gate the reflection impls behind a `reflect` crate feature, *without*
1138    /// gating json/views/text (unlike
1139    /// [`gate_impls_on_crate_features`](Self::gate_impls_on_crate_features),
1140    /// which gates them all together).
1141    ///
1142    /// Used by crates that ship view/text impls unconditionally but want the
1143    /// reflection surface — which pulls a `buffa-descriptor` dependency and
1144    /// `std` — to be opt-in. `buffa-types` is the motivating case: its WKT
1145    /// views are always available, but `impl ReflectMessage` for them is gated
1146    /// behind `buffa-types`'s `reflect` feature.
1147    ///
1148    /// When [`gate_impls_on_crate_features`](Self::gate_impls_on_crate_features)
1149    /// is already on, reflection is gated regardless and this flag is ignored.
1150    ///
1151    /// A low-level knob for crates whose generated code is a public interface
1152    /// (`buffa-types`, the conformance harness). Set directly by `gen_wkt_types`
1153    /// and exposed through `buffa_build::Config::gate_reflect_on_crate_feature`
1154    /// (currently `#[doc(hidden)]`, paired with the experimental vtable flag).
1155    ///
1156    /// Defaults to `false`.
1157    pub gate_reflect_on_crate_feature: bool,
1158    /// Emit idiomatic `UpperCamelCase` constant aliases alongside each enum
1159    /// variant.
1160    ///
1161    /// Protobuf style names enum values in `SHOUTY_SNAKE_CASE`, conventionally
1162    /// prefixed with the enum name (`RULE_LEVEL_HIGH`). Those names remain the
1163    /// definitive Rust variants — they are guaranteed unique and valid by
1164    /// protobuf, and existing references (including `Debug` output) are
1165    /// unchanged. When this is enabled, codegen additionally emits associated
1166    /// `const`s with the prefix stripped and the name converted to
1167    /// `UpperCamelCase` (`RULE_LEVEL_HIGH` → `High`), so downstream code can
1168    /// write `RuleLevel::High`.
1169    ///
1170    /// The conversion is lossy, so two values can collide (`FOO_BAR` and
1171    /// `FOO__BAR` both map to `FooBar`). The rule is all-or-nothing per enum:
1172    /// if any two values would collide after conversion, or a value would yield
1173    /// an invalid identifier, **no** aliases are emitted for that enum (a
1174    /// [`CodeGenWarning`] and an enum doc note explain why). This keeps every
1175    /// match either fully `SHOUTY_SNAKE_CASE` or fully idiomatic, never a forced
1176    /// mix.
1177    ///
1178    /// The aliases are associated `const`s, which work in pattern position too:
1179    /// a `match` written entirely against aliases is still exhaustiveness-checked
1180    /// (the "non-exhaustive" error names the underlying `SHOUTY_SNAKE_CASE`
1181    /// variant, since that is the canonical name).
1182    ///
1183    /// Defaults to `true`: the aliases are purely additive (the proto names
1184    /// remain the variants, and `Debug` is unchanged), so enabling by default is
1185    /// backward-compatible, and the all-or-nothing rule guarantees correctness on
1186    /// any enum.
1187    pub idiomatic_enum_aliases: bool,
1188    /// Emit `use`-backed short type names at the package root instead of
1189    /// fully-qualified paths, so generated code reads like hand-written
1190    /// Rust (`pub at: MessageField<Timestamp>` instead of
1191    /// `pub at: ::buffa::MessageField<::buffa_types::google::protobuf::Timestamp>`).
1192    ///
1193    /// Requires [`file_per_package`](Self::file_per_package): only there is
1194    /// the package-root scope a single-writer file whose complete name set
1195    /// is known at generation time. In the multi-file layout the stitcher
1196    /// `include!`-merges every proto's content files into the shared root
1197    /// scope, where emitted `use` directives could collide across files —
1198    /// [`generate`] returns an error for that combination rather than
1199    /// silently ignoring the flag.
1200    ///
1201    /// Off by default; default output is byte-for-byte unchanged. Short
1202    /// names are always backed by an explicit `use` (never glob reliance),
1203    /// are refused when they would collide with the package's own items or
1204    /// names referenced bare by sibling emissions, and fall back to
1205    /// parent-module qualification and then the fully-qualified path. The
1206    /// short-name *assignment* (use block and per-path choices) is computed
1207    /// from a collection pre-pass and is stable under `.proto` file
1208    /// reordering; item order within the file still follows input order,
1209    /// so whole-file output is not reorder-invariant. The pre-pass
1210    /// generates the package twice, roughly doubling codegen time for it.
1211    ///
1212    /// Scope: only package-root *type declarations* (struct fields, oneof
1213    /// `Option` wrappers) are shortened. Impl bodies, nested-message
1214    /// modules, and `__buffa` internals keep fully-qualified paths — the
1215    /// readability payoff lands where consumers look (struct definitions
1216    /// and rustdoc), not in the codec internals.
1217    ///
1218    /// **Experimental** means: the generated-output shape may change
1219    /// between releases (requiring regeneration of checked-in code), and
1220    /// the option itself may be renamed or removed outside semver
1221    /// guarantees.
1222    pub idiomatic_imports: bool,
1223    /// Crate feature names used by the `#[cfg(feature = "...")]` gates that
1224    /// [`gate_impls_on_crate_features`](Self::gate_impls_on_crate_features)
1225    /// and
1226    /// [`gate_reflect_on_crate_feature`](Self::gate_reflect_on_crate_feature)
1227    /// emit.
1228    ///
1229    /// Defaults to `"json"` / `"views"` / `"text"` / `"reflect"`. Override a
1230    /// name when the consuming crate gates the same concern behind a
1231    /// different feature name (e.g. its JSON support behind a `serde`
1232    /// feature). Inert unless one of the gating flags is on.
1233    pub feature_gate_names: FeatureGateNames,
1234    /// Prefix prepended to every locally-generated Rust type name.
1235    ///
1236    /// With prefix `"Rpc"`, `message User {}` generates `struct RpcUser`,
1237    /// its view becomes `RpcUserView` / `RpcUserOwnedView`, and every
1238    /// cross-reference (fields, oneof variants, maps, extensions) uses the
1239    /// prefixed name. Useful in multi-protocol systems where generated
1240    /// types from different domains would otherwise collide with each
1241    /// other or with a canonical hand-written model.
1242    ///
1243    /// The prefix applies to **message structs and enum types** (top-level
1244    /// and nested, plus their derived view/owned-view types). It does not
1245    /// apply to:
1246    ///
1247    /// - module names (`message Outer` still nests under `pub mod outer` —
1248    ///   modules are namespaced by the package tree and never collide with
1249    ///   type names),
1250    /// - oneof enums (structurally namespaced under `__buffa::oneof::`,
1251    ///   named after the oneof declaration, not the message),
1252    /// - types mapped away via [`extern_paths`](Self::extern_paths) or the
1253    ///   automatic well-known-type mapping (their names are owned by the
1254    ///   external crate),
1255    /// - wire-format and JSON output (proto names, `TYPE_URL`s, and JSON
1256    ///   field names are unaffected — this is a pure Rust-identifier
1257    ///   rename).
1258    ///
1259    /// When another codegen run references these prefixed types via its own
1260    /// [`extern_paths`](Self::extern_paths) mapping, the mapped Rust path
1261    /// must spell out the prefixed name (e.g. `::crate_a::RpcUser`) — the
1262    /// proto name carries no prefix, so the mapping is not derived
1263    /// automatically. Prefix-induced name collisions (e.g. `message RpcUser`
1264    /// alongside `message User` with prefix `Rpc`) are not detected here;
1265    /// they surface as ordinary duplicate-definition errors when the
1266    /// generated code is compiled.
1267    ///
1268    /// Must be PascalCase (`[A-Z][A-Za-z0-9]*`) — an ASCII uppercase letter
1269    /// followed by ASCII letters and digits — so the prefixed names stay
1270    /// conventionally cased; generation fails with
1271    /// [`CodeGenError::InvalidTypeNamePrefix`] otherwise. Defaults to `""`
1272    /// (no prefix).
1273    pub type_name_prefix: String,
1274}
1275
1276impl Default for CodeGenConfig {
1277    fn default() -> Self {
1278        Self {
1279            generate_views: true,
1280            lazy_views: false,
1281            preserve_unknown_fields: true,
1282            generate_json: false,
1283            generate_arbitrary: false,
1284            extern_paths: Vec::new(),
1285            bytes_fields: Vec::new(),
1286            string_fields: Vec::new(),
1287            map_fields: Vec::new(),
1288            pointer_fields: Vec::new(),
1289            repeated_fields: Vec::new(),
1290            unboxed_oneof_fields: Vec::new(),
1291            strict_utf8_mapping: false,
1292            allow_message_set: false,
1293            generate_text: false,
1294            emit_register_fn: true,
1295            file_per_package: false,
1296            type_attributes: Vec::new(),
1297            field_attributes: Vec::new(),
1298            message_attributes: Vec::new(),
1299            enum_attributes: Vec::new(),
1300            oneof_attributes: Vec::new(),
1301            gate_impls_on_crate_features: false,
1302            generate_with_setters: true,
1303            generate_reflection: false,
1304            generate_reflection_vtable: false,
1305            gate_reflect_on_crate_feature: false,
1306            idiomatic_enum_aliases: true,
1307            idiomatic_imports: false,
1308            feature_gate_names: FeatureGateNames::default(),
1309            type_name_prefix: String::new(),
1310        }
1311    }
1312}
1313
1314impl CodeGenConfig {
1315    /// Active [`feature_gates::FeatureGates`] for this config.
1316    ///
1317    /// Recomputed on each call (cheap — three boolean ANDs); call once at
1318    /// the top of a generation function and thread through, or call inline
1319    /// at each use site, whichever reads better.
1320    pub(crate) fn feature_gates(&self) -> feature_gates::FeatureGates<'_> {
1321        feature_gates::FeatureGates::for_config(self)
1322    }
1323
1324    /// Apply [`type_name_prefix`](Self::type_name_prefix) to a locally
1325    /// generated type's proto simple name, yielding the Rust identifier to
1326    /// declare (and register in the type map).
1327    pub(crate) fn prefixed_type_name(&self, proto_name: &str) -> String {
1328        format!("{}{proto_name}", self.type_name_prefix)
1329    }
1330
1331    /// Validate [`type_name_prefix`](Self::type_name_prefix): empty (no
1332    /// prefix) or PascalCase (`[A-Z][A-Za-z0-9]*`), so `{prefix}{TypeName}`
1333    /// is always a valid, conventionally-cased identifier that does not
1334    /// trip `non_camel_case_types` in consumer crates.
1335    pub(crate) fn validate_type_name_prefix(&self) -> Result<(), CodeGenError> {
1336        let prefix = &self.type_name_prefix;
1337        let valid = prefix.is_empty()
1338            || (prefix.starts_with(|c: char| c.is_ascii_uppercase())
1339                && prefix.chars().all(|c| c.is_ascii_alphanumeric()));
1340        if valid {
1341            Ok(())
1342        } else {
1343            Err(CodeGenError::InvalidTypeNamePrefix {
1344                prefix: prefix.clone(),
1345            })
1346        }
1347    }
1348}
1349
1350/// Compute the effective extern path list by starting with user-provided
1351/// mappings and adding the default WKT mapping if appropriate.
1352///
1353/// The default mapping `".google.protobuf" → "::buffa_types::google::protobuf"`
1354/// is added unless:
1355/// - The user already provided an extern_path covering `.google.protobuf`
1356/// - Any of the files being generated are in the `google.protobuf` package
1357///   (i.e., we're building `buffa-types` itself)
1358pub(crate) fn effective_extern_paths(
1359    file_descriptors: &[FileDescriptorProto],
1360    files_to_generate: &[String],
1361    config: &CodeGenConfig,
1362) -> Vec<(String, String)> {
1363    let mut paths = config.extern_paths.clone();
1364
1365    // Only an EXACT .google.protobuf mapping suppresses auto-injection.
1366    // A sub-package mapping like .google.protobuf.compiler does NOT cover
1367    // WKTs like Timestamp — resolve_extern_prefix's longest-prefix matching
1368    // lets both coexist, so we still inject the parent mapping.
1369    let has_wkt_mapping = paths.iter().any(|(proto, _)| proto == ".google.protobuf");
1370
1371    if !has_wkt_mapping {
1372        // Check if we're generating google.protobuf files ourselves
1373        // (e.g., building buffa-types). If so, don't auto-map.
1374        let generating_wkts = file_descriptors
1375            .iter()
1376            .filter(|fd| {
1377                fd.name
1378                    .as_deref()
1379                    .is_some_and(|n| files_to_generate.iter().any(|f| f == n))
1380            })
1381            .any(|fd| fd.package.as_deref() == Some("google.protobuf"));
1382
1383        if !generating_wkts {
1384            paths.push((
1385                ".google.protobuf".to_string(),
1386                "::buffa_types::google::protobuf".to_string(),
1387            ));
1388        }
1389    }
1390
1391    paths
1392}
1393
1394/// Compute the effective file-level extern path list.
1395///
1396/// File-level mappings route a specific `.proto` file to a Rust module root,
1397/// taking priority over the package-level mappings from
1398/// [`effective_extern_paths`]. They exist to resolve a structural problem:
1399/// `descriptor.proto` is in the same `google.protobuf` package as the
1400/// JSON-mappable WKTs (`Timestamp`, `Any`, …), but its types live in
1401/// `buffa-descriptor`, not `buffa-types`. A single package-keyed
1402/// `.google.protobuf` extern_path can route the package to one crate or the
1403/// other; it can't split it. The file-level mapping splits it.
1404///
1405/// Auto-injected mappings (when not suppressed):
1406///
1407/// | Proto file | Rust module |
1408/// |---|---|
1409/// | `google/protobuf/descriptor.proto` | `::buffa_descriptor::generated::descriptor` |
1410/// | `google/protobuf/compiler/plugin.proto` | `::buffa_descriptor::generated::compiler` |
1411///
1412/// Suppression conditions, evaluated **per file**:
1413///
1414/// - **A user-provided `extern_path` covers the file's package.** That
1415///   override has covered the file's types since the package mapping was
1416///   introduced; auto-injecting a higher-priority file-level mapping would
1417///   silently redirect them away from the user's crate. Matching is via
1418///   the same longest-prefix logic the package resolver uses, so both an
1419///   exact `.google.protobuf` mapping and a sub-package
1420///   `.google.protobuf.compiler` mapping suppress the entries they cover —
1421///   `.google.protobuf` suppresses both, `.google.protobuf.compiler`
1422///   suppresses only `plugin.proto`.
1423/// - **The proto file itself is in `files_to_generate`.** When building
1424///   `buffa-descriptor` (or any local copy of `descriptor.proto`), its types
1425///   must resolve to the local module, not externally.
1426///
1427/// Currently internal-only — there is no `CodeGenConfig` field for
1428/// user-provided *file-level* mappings. The user-facing `extern_path` API is
1429/// keyed by proto package *or* type FQN (per-type overrides, issue #111);
1430/// per-file overrides may be added later as a public feature if a concrete
1431/// need arises.
1432pub(crate) fn effective_file_extern_paths(
1433    files_to_generate: &[String],
1434    config: &CodeGenConfig,
1435) -> Vec<(String, String)> {
1436    // (proto file path, proto package, Rust module root). The package is
1437    // recorded alongside the file so the user-override suppression check
1438    // is per-file: a `.google.protobuf.compiler` extern_path covers only
1439    // `plugin.proto`, while `.google.protobuf` covers both.
1440    const DESCRIPTOR_FILES: [(&str, &str, &str); 2] = [
1441        (
1442            "google/protobuf/descriptor.proto",
1443            "google.protobuf",
1444            "::buffa_descriptor::generated::descriptor",
1445        ),
1446        (
1447            "google/protobuf/compiler/plugin.proto",
1448            "google.protobuf.compiler",
1449            "::buffa_descriptor::generated::compiler",
1450        ),
1451    ];
1452
1453    DESCRIPTOR_FILES
1454        .into_iter()
1455        .filter(|(proto_file, package, _)| {
1456            // Yield to a user package-level extern_path that already covers
1457            // this file's package: anyone who wrote
1458            // `extern_path(".google.protobuf", "::my_crate")` (or a
1459            // sub-package mapping) today routes these types to their crate;
1460            // the auto-injected file-level mapping must not silently
1461            // outrank it.
1462            if context::resolve_extern_prefix(package, &config.extern_paths).is_some() {
1463                return false;
1464            }
1465            // Don't externalize a file we're generating locally.
1466            !files_to_generate.iter().any(|f| f == proto_file)
1467        })
1468        .map(|(proto_file, _, rust_module)| (proto_file.to_string(), rust_module.to_string()))
1469        .collect()
1470}
1471
1472/// One CamelCase collision: a target identifier and the proto value names that
1473/// would all convert onto it.
1474///
1475/// Part of [`CodeGenWarning::IdiomaticAliasesSuppressed`].
1476#[derive(Debug, Clone, PartialEq, Eq)]
1477#[non_exhaustive]
1478pub struct AliasConflict {
1479    /// The `UpperCamelCase` identifier the colliding values map to.
1480    pub camel_target: String,
1481    /// The proto value names that convert onto `camel_target` (includes a
1482    /// literal variant name when an alias would shadow it).
1483    pub proto_values: Vec<String>,
1484}
1485
1486/// A non-fatal diagnostic produced during code generation.
1487///
1488/// Returned by [`generate_with_diagnostics`]. Render the human-readable form via
1489/// the [`Display`](core::fmt::Display) impl (e.g. `cargo:warning={warning}`), or
1490/// match on the variant for programmatic handling. The enum and its variants are
1491/// `#[non_exhaustive]` so new diagnostic kinds and fields can be added without a
1492/// breaking change.
1493#[derive(Debug, Clone, PartialEq, Eq)]
1494#[non_exhaustive]
1495pub enum CodeGenWarning {
1496    /// Idiomatic CamelCase aliases were suppressed for an enum because two or
1497    /// more proto values collide after conversion, or a value would convert to
1498    /// an invalid identifier. The enum's `SHOUTY_SNAKE_CASE` variants are
1499    /// unaffected.
1500    #[non_exhaustive]
1501    IdiomaticAliasesSuppressed {
1502        /// The Rust name of the affected enum.
1503        enum_name: String,
1504        /// Each collision, by target identifier. Empty if the only problem was
1505        /// invalid identifiers.
1506        conflicts: Vec<AliasConflict>,
1507        /// Proto values that would convert to an invalid Rust identifier.
1508        invalid: Vec<String>,
1509    },
1510    /// A field or oneof accessor on a generated `FooOwnedView` wrapper was
1511    /// suppressed because the proto name collides with one of the wrapper's
1512    /// reserved method names (`decode`, `view`, `bytes`, …). The field stays
1513    /// fully accessible through `view()` on the wrapper (or
1514    /// `OwnedView::reborrow`).
1515    #[non_exhaustive]
1516    OwnedViewAccessorSuppressed {
1517        /// The Rust name of the wrapper type (e.g. `FooOwnedView`).
1518        wrapper_name: String,
1519        /// The proto field or oneof name whose accessor was suppressed.
1520        field_name: String,
1521    },
1522    /// `lazy_views` was requested with `generate_views` disabled; the lazy
1523    /// family reuses the eager view-oneof enums and eager sub-view types, so
1524    /// no lazy views were generated. Emitted once per generation run.
1525    #[non_exhaustive]
1526    LazyViewsRequireViews,
1527}
1528
1529impl core::fmt::Display for CodeGenWarning {
1530    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1531        match self {
1532            Self::IdiomaticAliasesSuppressed {
1533                enum_name,
1534                conflicts,
1535                invalid,
1536            } => {
1537                // Name the cause accurately: a collision, an invalid identifier,
1538                // or both.
1539                let cause = match (conflicts.is_empty(), invalid.is_empty()) {
1540                    (false, true) => "naming conflict",
1541                    (true, false) => "invalid identifier",
1542                    _ => "naming conflict / invalid identifier",
1543                };
1544                write!(
1545                    f,
1546                    "enum `{enum_name}`: idiomatic CamelCase aliases suppressed ({cause})"
1547                )?;
1548                let mut parts: Vec<String> = conflicts
1549                    .iter()
1550                    .map(|c| format!("{} → {}", c.proto_values.join(", "), c.camel_target))
1551                    .collect();
1552                parts.extend(invalid.iter().map(|n| format!("{n} → invalid identifier")));
1553                if !parts.is_empty() {
1554                    write!(f, ": {}", parts.join("; "))?;
1555                }
1556                Ok(())
1557            }
1558            Self::OwnedViewAccessorSuppressed {
1559                wrapper_name,
1560                field_name,
1561            } => {
1562                write!(
1563                    f,
1564                    "`{wrapper_name}`: accessor for field `{field_name}` suppressed \
1565                     (collides with a reserved wrapper method); use `.view().{field_name}` instead"
1566                )
1567            }
1568            Self::LazyViewsRequireViews => {
1569                write!(
1570                    f,
1571                    "lazy_views requires generate_views (the lazy family reuses the \
1572                     eager view-oneof enums and sub-view types); no lazy views were \
1573                     generated — enable generate_views (buffa-build: \
1574                     `.generate_views(true)`, the default; plugin: `views=true`)"
1575                )
1576            }
1577        }
1578    }
1579}
1580
1581/// Generate Rust source files from a set of file descriptors.
1582///
1583/// `files_to_generate` is the set of file names that were explicitly requested
1584/// (matching `CodeGeneratorRequest.file_to_generate`). Descriptors for
1585/// dependencies may be present in `file_descriptors` but won't produce output
1586/// files unless they appear in `files_to_generate`.
1587///
1588/// Each `.proto` emits up to five content files (kinds with no content
1589/// are omitted); each distinct package emits one `<pkg>.mod.rs`
1590/// stitcher. Packages are processed in sorted order for deterministic
1591/// output.
1592///
1593/// # Diagnostics
1594///
1595/// Non-fatal diagnostics produced during generation (e.g. an enum whose
1596/// idiomatic CamelCase aliases were suppressed by a naming conflict) are
1597/// **discarded** here. Use [`generate_with_diagnostics`] to receive them and
1598/// surface them as build warnings.
1599pub fn generate(
1600    file_descriptors: &[FileDescriptorProto],
1601    files_to_generate: &[String],
1602    config: &CodeGenConfig,
1603) -> Result<Vec<GeneratedFile>, CodeGenError> {
1604    Ok(generate_with_diagnostics(file_descriptors, files_to_generate, config)?.0)
1605}
1606
1607/// Like [`generate`], but also returns the non-fatal [`CodeGenWarning`]s
1608/// collected during generation (e.g. enums whose idiomatic CamelCase aliases
1609/// were suppressed by a naming conflict).
1610///
1611/// Surface each warning via its [`Display`](core::fmt::Display) impl — e.g. as a
1612/// `cargo:warning=...` from a `build.rs`, or on stderr from a standalone
1613/// generator — or match on it for programmatic handling. [`generate`] discards
1614/// them, so existing callers are unaffected.
1615///
1616/// Warnings are returned only on success. On error, any warnings already
1617/// collected are dropped along with the partial output — the [`CodeGenError`]
1618/// is the actionable signal.
1619///
1620/// # Errors
1621///
1622/// Returns [`CodeGenError::FileNotFound`] if a name in `files_to_generate` has
1623/// no matching descriptor, [`CodeGenError::InvalidTypeNamePrefix`] if
1624/// [`CodeGenConfig::type_name_prefix`] is not empty or PascalCase,
1625/// [`CodeGenError::Other`] if `generate_reflection_vtable`
1626/// is set without `generate_reflection` or if an active feature-gate name in
1627/// [`CodeGenConfig::feature_gate_names`] is not a valid Cargo feature name,
1628/// and other [`CodeGenError`] variants for malformed descriptors (e.g. a
1629/// missing required field) encountered while generating.
1630/// Whether a custom `repeated` element type holds proto `string` or `bytes` —
1631/// selects `ValueRef::String`/`ValueRef::Bytes` and the JSON delegate module.
1632#[derive(Clone, Copy, PartialEq, Eq)]
1633enum CustomElemKind {
1634    String,
1635    Bytes,
1636}
1637
1638/// The custom owned types collected generation-wide that need a codegen-emitted
1639/// reflection / JSON impl, split by the trait each needs.
1640#[derive(Default)]
1641struct CustomElements {
1642    /// Types needing `ReflectElement` (+ `ProtoElemJson` for bytes): custom
1643    /// `repeated` elements, custom `map` *values* (`string` or `bytes`).
1644    elements: std::collections::BTreeMap<String, CustomElemKind>,
1645    /// Custom `string` types used as a `map` *key*: need `ReflectMapKey` (vtable
1646    /// reflection only — the bridge path keys maps by the borrowed `&str` view).
1647    map_keys: std::collections::BTreeSet<String>,
1648}
1649
1650/// Collect the distinct custom owned types that need a codegen-emitted element
1651/// impl (`ReflectElement` / `ProtoElemJson`), keyed by Rust type path, across
1652/// the whole request. These are custom `string`/`bytes` types used as the
1653/// element of a `repeated` field, and custom `bytes` types used as a
1654/// `map<K, bytes>` value — both reflect via the element trait and (for bytes)
1655/// serialize JSON via `proto_map`/`proto_seq`. Singular / optional / oneof
1656/// custom fields reach JSON and reflection without an element-trait impl, and
1657/// `string`/`Vec<u8>`/`Bytes` map values are covered by the built-in impls.
1658fn collect_custom_elements(
1659    ctx: &context::CodeGenContext,
1660    file_descriptors: &[FileDescriptorProto],
1661    files_to_generate: &[String],
1662) -> CustomElements {
1663    use crate::generated::descriptor::field_descriptor_proto::{Label, Type};
1664
1665    fn walk(
1666        ctx: &context::CodeGenContext,
1667        messages: &[crate::generated::descriptor::DescriptorProto],
1668        scope: &str,
1669        parent_features: &crate::features::ResolvedFeatures,
1670        out: &mut CustomElements,
1671    ) {
1672        for msg in messages {
1673            let name = msg.name.as_deref().unwrap_or("");
1674            let fqn = if scope.is_empty() {
1675                name.to_string()
1676            } else {
1677                format!("{scope}.{name}")
1678            };
1679            let msg_features = crate::features::resolve_child(
1680                parent_features,
1681                crate::features::message_features(msg),
1682            );
1683            for field in &msg.field {
1684                if field.label.unwrap_or_default() != Label::LABEL_REPEATED {
1685                    continue;
1686                }
1687                let field_name = field.name.as_deref().unwrap_or("");
1688                let field_fqn = format!(".{fqn}.{field_name}");
1689
1690                // `map` slots: a custom value type needs the element impls
1691                // (reflected via ReflectMap → ReflectElement, JSON via
1692                // proto_map → ProtoElemJson for bytes), and a custom `string`
1693                // key needs ReflectMapKey. All keyed on the outer map field
1694                // path (the same `string_type` rule covers both slots), with the
1695                // `map<bytes, bytes>` value carve-out.
1696                if let Some(entry) = crate::message::find_map_entry(msg, field) {
1697                    let key_ty = crate::message::map_entry_key_type(ctx, entry, &msg_features);
1698                    let val_ty = crate::message::map_entry_value_type(ctx, entry, &msg_features);
1699                    if let crate::BytesRepr::Custom(path) =
1700                        crate::impl_message::map_value_bytes_repr(
1701                            ctx, key_ty, val_ty, &fqn, field_name,
1702                        )
1703                    {
1704                        out.elements.entry(path).or_insert(CustomElemKind::Bytes);
1705                    }
1706                    if let crate::StringRepr::Custom(path) = ctx.string_repr(&field_fqn) {
1707                        if key_ty == Some(Type::TYPE_STRING) {
1708                            out.map_keys.insert(path.clone());
1709                        }
1710                        if val_ty == Some(Type::TYPE_STRING) {
1711                            out.elements.entry(path).or_insert(CustomElemKind::String);
1712                        }
1713                    }
1714                    continue;
1715                }
1716
1717                let field_features = crate::features::resolve_field(ctx, field, &msg_features);
1718                let ty = crate::impl_message::effective_type(ctx, field, &field_features);
1719                match ty {
1720                    Type::TYPE_STRING => {
1721                        if let crate::StringRepr::Custom(path) = ctx.string_repr(&field_fqn) {
1722                            out.elements.entry(path).or_insert(CustomElemKind::String);
1723                        }
1724                    }
1725                    Type::TYPE_BYTES => {
1726                        if let crate::BytesRepr::Custom(path) = ctx.bytes_repr(&field_fqn) {
1727                            out.elements.entry(path).or_insert(CustomElemKind::Bytes);
1728                        }
1729                    }
1730                    _ => {}
1731                }
1732            }
1733            walk(ctx, &msg.nested_type, &fqn, &msg_features, out);
1734        }
1735    }
1736
1737    let mut out = CustomElements::default();
1738    for file_name in files_to_generate {
1739        let Some(file) = file_descriptors
1740            .iter()
1741            .find(|f| f.name.as_deref() == Some(file_name.as_str()))
1742        else {
1743            continue;
1744        };
1745        let pkg = file.package.as_deref().unwrap_or("");
1746        let file_features = crate::features::for_file(file);
1747        walk(ctx, &file.message_type, pkg, &file_features, &mut out);
1748    }
1749    out
1750}
1751
1752/// Render the deduped `ProtoElemJson` / `ReflectElement` impls for the collected
1753/// custom element types (repeated elements and `map<K, bytes>` values). Each
1754/// impl is feature-gated so a non-JSON /
1755/// non-reflect build never references an absent trait. These compile only when
1756/// the custom type is local to the generating crate (the orphan rule); that is
1757/// the documented limitation of a custom `repeated` element under JSON or vtable
1758/// reflection.
1759fn render_custom_elem_impls(
1760    ctx: &context::CodeGenContext,
1761    elems: &CustomElements,
1762) -> Result<TokenStream, CodeGenError> {
1763    let json_gate = ctx.config.feature_gates().json;
1764    let reflect_gate = ctx.config.feature_gates().reflect;
1765    let mut out = TokenStream::new();
1766    for (path, kind) in &elems.elements {
1767        let ty = parse_custom_type_path(path)?;
1768        // `ProtoElemJson` is only needed for the `bytes` element path (proto3
1769        // JSON base64). A repeated `string` element serializes through the
1770        // native `Vec<T>` serde derive, and custom `string` map keys/values go
1771        // through serde too (the derive / `string_key_map` / `proto_str_key_map`
1772        // paths), so a String-kind `ProtoElemJson` impl would be dead code.
1773        if ctx.config.generate_json && *kind == CustomElemKind::Bytes {
1774            out.extend(feature_gates::cfg_block(
1775                quote! {
1776                    impl ::buffa::json_helpers::ProtoElemJson for #ty {
1777                        fn serialize_proto_json<S: ::serde::Serializer>(
1778                            v: &Self,
1779                            s: S,
1780                        ) -> ::core::result::Result<S::Ok, S::Error> {
1781                            ::buffa::json_helpers::bytes::serialize(
1782                                ::core::convert::AsRef::<[u8]>::as_ref(v),
1783                                s,
1784                            )
1785                        }
1786                        fn deserialize_proto_json<'de, D: ::serde::Deserializer<'de>>(
1787                            d: D,
1788                        ) -> ::core::result::Result<Self, D::Error> {
1789                            ::buffa::json_helpers::bytes::deserialize(d)
1790                        }
1791                    }
1792                },
1793                json_gate,
1794            ));
1795        }
1796        if ctx.config.generate_reflection_vtable {
1797            let value_ref = match kind {
1798                CustomElemKind::String => quote! {
1799                    ::buffa_descriptor::reflect::ValueRef::String(
1800                        ::core::convert::AsRef::<str>::as_ref(self),
1801                    )
1802                },
1803                CustomElemKind::Bytes => quote! {
1804                    ::buffa_descriptor::reflect::ValueRef::Bytes(
1805                        ::core::convert::AsRef::<[u8]>::as_ref(self),
1806                    )
1807                },
1808            };
1809            out.extend(feature_gates::cfg_block(
1810                quote! {
1811                    impl ::buffa_descriptor::reflect::ReflectElement for #ty {
1812                        fn as_value_ref(&self) -> ::buffa_descriptor::reflect::ValueRef<'_> {
1813                            #value_ref
1814                        }
1815                    }
1816                },
1817                reflect_gate,
1818            ));
1819        }
1820    }
1821    // A custom `string` type used as a `map` key needs `ReflectMapKey` for
1822    // vtable reflection (the bridge path keys maps by the borrowed `&str` view,
1823    // which already implements it). Like the element impls above, this compiles
1824    // only when the type is local to the generating crate (the orphan rule).
1825    if ctx.config.generate_reflection_vtable {
1826        for path in &elems.map_keys {
1827            let ty = parse_custom_type_path(path)?;
1828            out.extend(feature_gates::cfg_block(
1829                quote! {
1830                    impl ::buffa_descriptor::reflect::ReflectMapKey for #ty {
1831                        fn as_map_key_ref(&self) -> ::buffa_descriptor::reflect::MapKeyRef<'_> {
1832                            ::buffa_descriptor::reflect::MapKeyRef::String(
1833                                ::core::convert::AsRef::<str>::as_ref(self),
1834                            )
1835                        }
1836                    }
1837                },
1838                reflect_gate,
1839            ));
1840        }
1841    }
1842    Ok(out)
1843}
1844
1845pub fn generate_with_diagnostics(
1846    file_descriptors: &[FileDescriptorProto],
1847    files_to_generate: &[String],
1848    config: &CodeGenConfig,
1849) -> Result<(Vec<GeneratedFile>, Vec<CodeGenWarning>), CodeGenError> {
1850    // Vtable reflection resolves against the per-package descriptor pool, which
1851    // is emitted by bridge-mode reflection — so it requires `generate_reflection`.
1852    // It does NOT require views: the owned `impl ReflectMessage` is self-contained,
1853    // so with views off, vtable mode still emits owned-message reflection (the
1854    // view impls are simply skipped along with the views).
1855    if config.generate_reflection_vtable && !config.generate_reflection {
1856        return Err(CodeGenError::Other(
1857            "generate_reflection_vtable requires generate_reflection to be enabled \
1858             (it provides the descriptor pool the reflect impls resolve against)"
1859                .into(),
1860        ));
1861    }
1862
1863    // Idiomatic imports place `use` directives in the package-root scope,
1864    // which is only single-writer (collision-free by construction) when the
1865    // whole package is one generated file.
1866    if config.idiomatic_imports && !config.file_per_package {
1867        return Err(CodeGenError::Other(
1868            "idiomatic_imports requires file_per_package to be enabled (the multi-file \
1869             layout include!-merges every proto's content into the shared package root, \
1870             where emitted `use` directives could collide across files)"
1871                .into(),
1872        ));
1873    }
1874
1875    // Active feature-gate names are emitted verbatim into
1876    // `#[cfg(feature = "...")]`; an invalid name fails open (the cfg is
1877    // permanently false and the gated impls silently compile away), so it
1878    // must be a hard error here rather than a debug assertion — build
1879    // scripts and protoc plugins typically run as release builds.
1880    if let Err((kind, name)) = config.feature_gates().validate() {
1881        return Err(CodeGenError::Other(format!(
1882            "invalid {kind} feature-gate name {name:?}: a Cargo feature name starts \
1883             with an ASCII alphanumeric or '_' and contains only alphanumerics, \
1884             '_', '-', '+', or '.'; an invalid name would leave the emitted \
1885             #[cfg(feature = ...)] permanently false, silently compiling the \
1886             gated impls away"
1887        )));
1888    }
1889
1890    config.validate_type_name_prefix()?;
1891
1892    let ctx = context::CodeGenContext::for_generate(file_descriptors, files_to_generate, config);
1893
1894    // Lazy views need the eager view machinery; warn once per run.
1895    if config.lazy_views && !config.generate_views {
1896        ctx.warn(CodeGenWarning::LazyViewsRequireViews);
1897    }
1898
1899    // Group requested files by package. BTreeMap → deterministic output order.
1900    let mut by_package: std::collections::BTreeMap<String, Vec<&FileDescriptorProto>> =
1901        std::collections::BTreeMap::new();
1902    for file_name in files_to_generate {
1903        let file_desc = file_descriptors
1904            .iter()
1905            .find(|f| f.name.as_deref() == Some(file_name.as_str()))
1906            .ok_or_else(|| CodeGenError::FileNotFound(file_name.clone()))?;
1907        let pkg = file_desc.package.as_deref().unwrap_or("").to_string();
1908        by_package.entry(pkg).or_default().push(file_desc);
1909    }
1910
1911    // Reflection: serialize the FileDescriptorSet once, regardless of how
1912    // many packages are in the request. Each package embeds its own copy of
1913    // the bytes (binary-size dedup is a follow-up), but the build-time
1914    // re-encoding cost shouldn't scale with the package count.
1915    let fds_bytes = if config.generate_reflection {
1916        reflect::encode_fds_once(file_descriptors)
1917    } else {
1918        Vec::new()
1919    };
1920
1921    // Custom owned types used as elements of a `repeated` field need a
1922    // `ProtoElemJson` (JSON) and/or `ReflectElement` (vtable) impl, which buffa
1923    // cannot provide for a foreign type (orphan rule). Collect them once across
1924    // the whole request, render the impls, and hand them to the first package so
1925    // they are emitted exactly once (a per-package emit would collide, E0119).
1926    let custom_elems = collect_custom_elements(&ctx, file_descriptors, files_to_generate);
1927    let custom_elem_impls = render_custom_elem_impls(&ctx, &custom_elems)?;
1928
1929    let empty_impls = TokenStream::new();
1930    let mut output = Vec::new();
1931    let mut custom_emitted = false;
1932    for (package, files) in by_package {
1933        let impls = if custom_emitted {
1934            &empty_impls
1935        } else {
1936            custom_emitted = true;
1937            &custom_elem_impls
1938        };
1939        generate_package(&ctx, &package, &files, &fds_bytes, impls, &mut output)?;
1940    }
1941
1942    Ok((output, ctx.take_warnings()))
1943}
1944
1945/// Generate a module tree that assembles per-package `.mod.rs` files into
1946/// nested `pub mod` blocks matching the protobuf package hierarchy.
1947///
1948/// Each entry is a `(mod_file_name, package)` pair where `package` is the
1949/// dot-separated protobuf package name (e.g., `"google.api"`) and
1950/// `mod_file_name` is the corresponding `<pkg>.mod.rs` (only
1951/// [`GeneratedFileKind::PackageMod`] outputs need wiring; per-proto
1952/// content files are reached via `include!` from the stitcher).
1953///
1954/// `include_mode` controls how `include!` paths are emitted.
1955///
1956/// `emit_inner_allow` adds a `#![allow(...)]` inner attribute at the top —
1957/// valid when the output is used directly as a module file (`mod.rs`),
1958/// invalid when consumed via `include!`.
1959pub fn generate_module_tree<F: AsRef<str>, P: AsRef<str>>(
1960    entries: &[(F, P)],
1961    include_mode: IncludeMode<'_>,
1962    emit_inner_allow: bool,
1963) -> String {
1964    use std::collections::BTreeMap;
1965    use std::fmt::Write;
1966
1967    use crate::idents::escape_mod_ident;
1968
1969    #[derive(Default)]
1970    struct ModNode {
1971        files: Vec<String>,
1972        children: BTreeMap<String, Self>,
1973    }
1974
1975    let mut root = ModNode::default();
1976
1977    for (file_name, package) in entries {
1978        let package = package.as_ref();
1979        let pkg_parts: Vec<&str> = if package.is_empty() {
1980            vec![]
1981        } else {
1982            package.split('.').collect()
1983        };
1984
1985        let mut node = &mut root;
1986        for seg in &pkg_parts {
1987            node = node.children.entry(seg.to_string()).or_default();
1988        }
1989        node.files.push(file_name.as_ref().to_string());
1990    }
1991
1992    let lints = ALLOW_LINTS.join(", ");
1993    let mut out = String::new();
1994    let _ = writeln!(out, "// @generated by buffa-codegen. DO NOT EDIT.");
1995    if emit_inner_allow {
1996        let _ = writeln!(out, "#![allow({lints})]");
1997    }
1998    let _ = writeln!(out);
1999
2000    fn emit(out: &mut String, node: &ModNode, depth: usize, mode: IncludeMode<'_>, lints: &str) {
2001        let indent = "    ".repeat(depth);
2002
2003        for file in &node.files {
2004            match mode {
2005                IncludeMode::Relative(prefix) => {
2006                    let _ = writeln!(out, r#"{indent}include!("{prefix}{file}");"#);
2007                }
2008                IncludeMode::OutDir => {
2009                    let _ = writeln!(
2010                        out,
2011                        r#"{indent}include!(concat!(env!("OUT_DIR"), "/{file}"));"#
2012                    );
2013                }
2014            }
2015        }
2016
2017        for (name, child) in &node.children {
2018            let escaped = escape_mod_ident(name);
2019            let _ = writeln!(out, "{indent}#[allow({lints})]");
2020            let _ = writeln!(out, "{indent}pub mod {escaped} {{");
2021            let _ = writeln!(out, "{indent}    use super::*;");
2022            emit(out, child, depth + 1, mode, lints);
2023            let _ = writeln!(out, "{indent}}}");
2024        }
2025    }
2026
2027    emit(&mut out, &root, 0, include_mode, &lints);
2028    out
2029}
2030
2031/// How [`generate_module_tree`] emits `include!` paths.
2032#[derive(Debug, Clone, Copy)]
2033pub enum IncludeMode<'a> {
2034    /// `include!("<prefix><file>")` — relative to the including file.
2035    /// Prefix is typically `""` or `"gen/"`.
2036    Relative(&'a str),
2037    /// `include!(concat!(env!("OUT_DIR"), "/<file>"))` — for build.rs output.
2038    OutDir,
2039}
2040
2041/// Validate one input descriptor before generating code for it.
2042///
2043/// Checks, in one walk of the message tree:
2044///
2045/// - **Reserved field names**: no field starts with `__buffa_` (would clash
2046///   with generated `__buffa_unknown_fields` / `__buffa_cached_size`).
2047/// - **Module-name conflicts**: no two sibling messages snake_case to the
2048///   same module name (e.g. `HTTPRequest` vs `HttpRequest`).
2049/// - **Reserved sentinel**: no package segment, message-module name, or
2050///   file-level enum name equals [`SENTINEL_MOD`](context::SENTINEL_MOD).
2051///   Ancillary types live under `pkg::__buffa::…`; a proto element
2052///   emitting an item named `__buffa` at package root would produce
2053///   E0428 against `pub mod __buffa`. This is the only name buffa
2054///   reserves in user namespace.
2055fn validate_file(file: &FileDescriptorProto) -> Result<(), CodeGenError> {
2056    use std::collections::HashMap;
2057
2058    let sentinel = context::SENTINEL_MOD;
2059    let package = file.package.as_deref().unwrap_or("");
2060    if package.split('.').any(|seg| seg == sentinel) {
2061        return Err(CodeGenError::ReservedModuleName {
2062            name: sentinel.to_string(),
2063            location: format!("package '{package}'"),
2064        });
2065    }
2066    // File-level enums emit `pub enum <name>` at package root with the
2067    // proto name preserved verbatim (no PascalCase normalization), so a
2068    // proto `enum __buffa` would land beside `pub mod __buffa`. Nested
2069    // enums live inside their owner message's module and cannot collide
2070    // with the package-root sentinel, so only file-level is checked.
2071    for enum_type in &file.enum_type {
2072        let name = enum_type.name.as_deref().unwrap_or("");
2073        if name == sentinel {
2074            return Err(CodeGenError::ReservedModuleName {
2075                name: sentinel.to_string(),
2076                location: format!("enum '{package}.{name}'"),
2077            });
2078        }
2079    }
2080
2081    fn walk(
2082        messages: &[crate::generated::descriptor::DescriptorProto],
2083        scope: &str,
2084        sentinel: &str,
2085    ) -> Result<(), CodeGenError> {
2086        // snake_case module name → original proto name (for conflict diag).
2087        let mut seen: HashMap<String, &str> = HashMap::new();
2088
2089        for msg in messages {
2090            let name = msg.name.as_deref().unwrap_or("");
2091            let fqn = if scope.is_empty() {
2092                name.to_string()
2093            } else {
2094                format!("{scope}.{name}")
2095            };
2096
2097            for field in &msg.field {
2098                if let Some(fname) = &field.name {
2099                    if fname.starts_with("__buffa_") {
2100                        return Err(CodeGenError::ReservedFieldName {
2101                            message_name: fqn,
2102                            field_name: fname.clone(),
2103                        });
2104                    }
2105                }
2106            }
2107
2108            let module_name = crate::oneof::to_snake_case(name);
2109            if module_name == sentinel {
2110                return Err(CodeGenError::ReservedModuleName {
2111                    name: sentinel.to_string(),
2112                    location: format!("message '{fqn}'"),
2113                });
2114            }
2115            if let Some(existing) = seen.get(&module_name) {
2116                return Err(CodeGenError::ModuleNameConflict {
2117                    scope: scope.to_string(),
2118                    name_a: existing.to_string(),
2119                    name_b: name.to_string(),
2120                    module_name,
2121                });
2122            }
2123            seen.insert(module_name, name);
2124
2125            walk(&msg.nested_type, &fqn, sentinel)?;
2126        }
2127        Ok(())
2128    }
2129
2130    walk(&file.message_type, package, sentinel)
2131}
2132
2133/// Per-proto content streams plus the file stem, ready to be formatted.
2134struct ProtoContent {
2135    stem: String,
2136    owned: TokenStream,
2137    view: TokenStream,
2138    lazy_view: TokenStream,
2139    oneof: TokenStream,
2140    view_oneof: TokenStream,
2141    ext: TokenStream,
2142    /// Candidate `pub use` re-exports targeting the package root (top-level
2143    /// view structs, file-level extension consts). Filtered against the
2144    /// package-wide root namespace in [`generate_package_mod`] — the package
2145    /// can span multiple `.proto` files, so collisions are only knowable at
2146    /// the stitcher level.
2147    root_reexports: Vec<message::ReexportCandidate>,
2148}
2149
2150/// Generate the per-`.proto` content token streams for one input file.
2151/// Each ancillary kind that has no content yields an empty stream and
2152/// is dropped at the file-emission stage.
2153fn generate_proto_content(
2154    ctx: &context::CodeGenContext,
2155    current_package: &str,
2156    file: &FileDescriptorProto,
2157    reg: &mut message::RegistryPaths,
2158) -> Result<ProtoContent, CodeGenError> {
2159    use crate::idents::make_field_ident;
2160    use crate::message::MessageOutput;
2161
2162    validate_file(file)?;
2163
2164    let resolver = imports::ImportResolver::new();
2165    let features = crate::features::for_file(file);
2166
2167    let mut owned = TokenStream::new();
2168    let mut view = TokenStream::new();
2169    let mut lazy_view = TokenStream::new();
2170    let mut oneof = TokenStream::new();
2171    let mut view_oneof = TokenStream::new();
2172    let mut ext = TokenStream::new();
2173    let mut root_reexports: Vec<message::ReexportCandidate> = Vec::new();
2174    let sentinel = make_field_ident(context::SENTINEL_MOD);
2175
2176    for enum_type in &file.enum_type {
2177        let enum_proto_name = enum_type.name.as_deref().unwrap_or("");
2178        let enum_rust_name = ctx.config.prefixed_type_name(enum_proto_name);
2179        let enum_fqn = if current_package.is_empty() {
2180            enum_proto_name.to_string()
2181        } else {
2182            format!("{}.{}", current_package, enum_proto_name)
2183        };
2184        owned.extend(enumeration::generate_enum(
2185            ctx,
2186            enum_type,
2187            &enum_rust_name,
2188            &enum_fqn,
2189            &features,
2190            &resolver,
2191        )?);
2192    }
2193
2194    for message_type in &file.message_type {
2195        let top_level_name = message_type.name.as_deref().unwrap_or("");
2196        let rust_name = ctx.config.prefixed_type_name(top_level_name);
2197        let proto_fqn = if current_package.is_empty() {
2198            top_level_name.to_string()
2199        } else {
2200            format!("{}.{}", current_package, top_level_name)
2201        };
2202        let MessageOutput {
2203            owned_top,
2204            owned_mod,
2205            oneof_tree: msg_oneof,
2206            view_tree: msg_view,
2207            lazy_view_tree: msg_lazy_view,
2208            view_oneof_tree: msg_view_oneof,
2209            reg: msg_reg,
2210        } = message::generate_message(
2211            ctx,
2212            message_type,
2213            current_package,
2214            &rust_name,
2215            &proto_fqn,
2216            &features,
2217            &resolver,
2218        )?;
2219        owned.extend(owned_top);
2220        let mod_name = ctx.nested_module_name(current_package, top_level_name);
2221        let mod_ident = make_field_ident(&mod_name);
2222        // When the nested-types module was deconflicted from a sub-package
2223        // (issue #135), document why the name carries a trailing `_`.
2224        let mod_doc = if mod_name == crate::oneof::to_snake_case(top_level_name) {
2225            quote! {}
2226        } else {
2227            let doc = format!(
2228                "Nested items of `{top_level_name}`. The module name carries a \
2229                 trailing `_` to avoid a collision with another module in this \
2230                 scope (a sub-package or sibling message of the same name). See \
2231                 buffa#135."
2232            );
2233            quote! { #[doc = #doc] }
2234        };
2235        for p in msg_reg.json_ext {
2236            reg.json_ext.push(quote! { #mod_ident :: #p });
2237        }
2238        for p in msg_reg.text_ext {
2239            reg.text_ext.push(quote! { #mod_ident :: #p });
2240        }
2241        reg.json_any.extend(msg_reg.json_any);
2242        reg.text_any.extend(msg_reg.text_any);
2243
2244        if !owned_mod.is_empty() {
2245            owned.extend(quote! {
2246                #mod_doc
2247                pub mod #mod_ident {
2248                    #[allow(unused_imports)]
2249                    use super::*;
2250                    #owned_mod
2251                }
2252            });
2253        }
2254        oneof.extend(msg_oneof);
2255        view.extend(msg_view);
2256        lazy_view.extend(msg_lazy_view);
2257        view_oneof.extend(msg_view_oneof);
2258
2259        // Top-level message view → re-export at package root. The leading
2260        // `self::` is load-bearing: when consumers nest packages with
2261        // `pub mod a { use super::*; pub mod a_b { use super::*; … } }`
2262        // (`buffa-build`'s `_include.rs` does this), a parent package's
2263        // `__buffa` is in scope via the glob, and Rust's import-resolution
2264        // pass treats a glob-imported name as ambiguous against a
2265        // **macro-expanded** local one (the `pub mod __buffa` block arrives
2266        // via `include!()`), even though a non-macro local definition would
2267        // shadow the glob — see rustc E0659. `self::` resolves it
2268        // deterministically. `#[doc(inline)]` makes rustdoc render the type's
2269        // full page at the natural path instead of a "Re-export of …" stub.
2270        if ctx.config.generate_views {
2271            let view_ident = format_ident!("{rust_name}View");
2272            root_reexports.push(message::ReexportCandidate {
2273                name: view_ident.to_string(),
2274                tokens: feature_gates::cfg_block(
2275                    quote! {
2276                        #[doc(inline)]
2277                        pub use self :: #sentinel :: view :: #view_ident;
2278                    },
2279                    ctx.config.feature_gates().views,
2280                ),
2281            });
2282            // The owned-view wrapper gets the same natural-path treatment as
2283            // the view struct, so `pkg::FooOwnedView` works out of the box.
2284            let owned_view_ident = format_ident!("{rust_name}OwnedView");
2285            root_reexports.push(message::ReexportCandidate {
2286                name: owned_view_ident.to_string(),
2287                tokens: feature_gates::cfg_block(
2288                    quote! {
2289                        #[doc(inline)]
2290                        pub use self :: #sentinel :: view :: #owned_view_ident;
2291                    },
2292                    ctx.config.feature_gates().views,
2293                ),
2294            });
2295            if ctx.config.lazy_views {
2296                let lazy_ident = format_ident!("{rust_name}LazyView");
2297                root_reexports.push(message::ReexportCandidate {
2298                    name: lazy_ident.to_string(),
2299                    tokens: feature_gates::cfg_block(
2300                        quote! {
2301                            #[doc(inline)]
2302                            pub use self :: #sentinel :: lazy_view :: #lazy_ident;
2303                        },
2304                        ctx.config.feature_gates().views,
2305                    ),
2306                });
2307            }
2308        }
2309    }
2310
2311    // File-level `extend` declarations → `__buffa::ext::` (depth 2).
2312    let (file_ext_tokens, file_ext_json, file_ext_text) = extension::generate_extensions(
2313        ctx,
2314        &file.extension,
2315        current_package,
2316        2,
2317        &features,
2318        current_package,
2319    )?;
2320    ext.extend(file_ext_tokens);
2321    for id in file_ext_json {
2322        reg.json_ext.push(quote! { #sentinel :: ext :: #id });
2323    }
2324    for id in file_ext_text {
2325        reg.text_ext.push(quote! { #sentinel :: ext :: #id });
2326    }
2327    // File-level extension consts → re-export at package root. `self::` and
2328    // `#[doc(inline)]` for the same reasons as the view re-exports above.
2329    for ext_field in &file.extension {
2330        let const_ident = extension::extension_const_ident(ext_field.name.as_deref().unwrap_or(""));
2331        root_reexports.push(message::ReexportCandidate {
2332            name: const_ident.to_string(),
2333            tokens: quote! {
2334                #[doc(inline)]
2335                pub use self :: #sentinel :: ext :: #const_ident;
2336            },
2337        });
2338    }
2339
2340    Ok(ProtoContent {
2341        stem: proto_path_to_stem(file.name.as_deref().unwrap_or("")),
2342        owned,
2343        view,
2344        lazy_view,
2345        oneof,
2346        view_oneof,
2347        ext,
2348        root_reexports,
2349    })
2350}
2351
2352/// Per-section token streams for one package, ready for the stitcher.
2353///
2354/// In per-file mode each section holds `include!("<stem>...rs")` calls; in
2355/// `file_per_package` mode each holds the actual generated items.
2356#[derive(Default)]
2357struct PackageSections {
2358    owned: Vec<TokenStream>,
2359    view: Vec<TokenStream>,
2360    lazy_view: Vec<TokenStream>,
2361    oneof: Vec<TokenStream>,
2362    view_oneof: Vec<TokenStream>,
2363    ext: Vec<TokenStream>,
2364}
2365
2366impl PackageSections {
2367    /// Append one proto file's generated items in-line.
2368    ///
2369    /// Empty streams are skipped so each section's emptiness reflects
2370    /// "the package has no content of this kind" — symmetric with the
2371    /// per-file branch that filters at file-emission time.
2372    fn push_inline(&mut self, pc: ProtoContent) {
2373        let push_if_nonempty = |dst: &mut Vec<TokenStream>, ts: TokenStream| {
2374            if !ts.is_empty() {
2375                dst.push(ts);
2376            }
2377        };
2378        push_if_nonempty(&mut self.owned, pc.owned);
2379        push_if_nonempty(&mut self.view, pc.view);
2380        push_if_nonempty(&mut self.lazy_view, pc.lazy_view);
2381        push_if_nonempty(&mut self.oneof, pc.oneof);
2382        push_if_nonempty(&mut self.view_oneof, pc.view_oneof);
2383        push_if_nonempty(&mut self.ext, pc.ext);
2384    }
2385}
2386
2387/// Generate all output files for one proto package: up to five content
2388/// files per `.proto` (empty ancillary kinds are skipped) plus one
2389/// `<pkg>.mod.rs` stitcher, or a single `<pkg>.rs` when
2390/// [`CodeGenConfig::file_per_package`] is set.
2391fn generate_package(
2392    ctx: &context::CodeGenContext,
2393    current_package: &str,
2394    files: &[&FileDescriptorProto],
2395    fds_bytes: &[u8],
2396    // Deduped `ProtoElemJson` / `ReflectElement` impls for custom repeated
2397    // element types, collected generation-wide and emitted into exactly one
2398    // package's `__buffa` module (empty for every package but the first).
2399    custom_elem_impls: &TokenStream,
2400    out: &mut Vec<GeneratedFile>,
2401) -> Result<(), CodeGenError> {
2402    // Registry paths are package-root-relative; `register_types` lives at
2403    // `__buffa::register_types` (one level deep), so each path gets a
2404    // single `super::` prefix when emitted into the fn body.
2405    let mut reg = message::RegistryPaths::default();
2406    let mut root_reexports: Vec<message::ReexportCandidate> = Vec::new();
2407
2408    // Idiomatic imports: dry-run the package's generation once with the
2409    // registry collecting, so the set of package-root path references is
2410    // known — by construction, exactly the set the real pass will emit —
2411    // then assign short names and generate for real with the registry
2412    // resolving. Generation is deterministic, so the two passes see the
2413    // same references; assignment sorts the collected set, so the result
2414    // is also stable under `.proto` file reordering. The dry run's other
2415    // outputs (tokens, registry paths, re-export candidates, warnings) are
2416    // discarded; only the candidate *names* feed the occupied set, since a
2417    // surviving re-export occupies a root name a `use` must not claim.
2418    if ctx.config.idiomatic_imports && ctx.config.file_per_package {
2419        ctx.imports_begin_collecting();
2420        let warn_mark = ctx.warnings_len();
2421        let mut scratch_reg = message::RegistryPaths::default();
2422        let mut occupied = root_occupied_names(ctx, files);
2423        for file in files {
2424            let pc = generate_proto_content(ctx, current_package, file, &mut scratch_reg)?;
2425            occupied.extend(pc.root_reexports.into_iter().map(|c| c.name));
2426        }
2427        ctx.truncate_warnings(warn_mark);
2428        occupied.insert("register_types".to_string());
2429        // The reflection pool accessor is re-exported at the package root
2430        // directly by `generate_package_mod` (not via a ReexportCandidate),
2431        // so the dry run doesn't capture it — reserve it explicitly.
2432        if ctx.config.generate_reflection {
2433            occupied.insert("descriptor_pool".to_string());
2434        }
2435        let collected = ctx.imports_take_collected();
2436        ctx.imports_set_resolving(imports::RootImports::assign(&collected, &occupied));
2437    }
2438
2439    let sections = if ctx.config.file_per_package {
2440        let mut sections = PackageSections::default();
2441        for file in files {
2442            let mut pc = generate_proto_content(ctx, current_package, file, &mut reg)?;
2443            root_reexports.append(&mut pc.root_reexports);
2444            sections.push_inline(pc);
2445        }
2446        sections
2447    } else {
2448        let mut sections = PackageSections::default();
2449        for file in files {
2450            let mut pc = generate_proto_content(ctx, current_package, file, &mut reg)?;
2451            root_reexports.append(&mut pc.root_reexports);
2452            let source = file.name.as_deref().unwrap_or("");
2453            let stem = pc.stem;
2454
2455            // Empty ancillary token streams are skipped — neither the
2456            // content file nor the stitcher's `include!` is emitted.
2457            let emit = |suffix: &str,
2458                        kind: GeneratedFileKind,
2459                        tokens: TokenStream,
2460                        section: &mut Vec<TokenStream>,
2461                        out: &mut Vec<GeneratedFile>|
2462             -> Result<(), CodeGenError> {
2463                if tokens.is_empty() {
2464                    return Ok(());
2465                }
2466                let name = format!("{stem}{suffix}.rs");
2467                section.push(quote! { include!(#name); });
2468                out.push(GeneratedFile {
2469                    name,
2470                    package: current_package.to_string(),
2471                    kind,
2472                    content: format_tokens(tokens, source)?,
2473                });
2474                Ok(())
2475            };
2476            emit(
2477                "",
2478                GeneratedFileKind::Owned,
2479                pc.owned,
2480                &mut sections.owned,
2481                out,
2482            )?;
2483            emit(
2484                ".__view",
2485                GeneratedFileKind::View,
2486                pc.view,
2487                &mut sections.view,
2488                out,
2489            )?;
2490            emit(
2491                ".__lazy_view",
2492                GeneratedFileKind::LazyView,
2493                pc.lazy_view,
2494                &mut sections.lazy_view,
2495                out,
2496            )?;
2497            emit(
2498                ".__oneof",
2499                GeneratedFileKind::Oneof,
2500                pc.oneof,
2501                &mut sections.oneof,
2502                out,
2503            )?;
2504            emit(
2505                ".__view_oneof",
2506                GeneratedFileKind::ViewOneof,
2507                pc.view_oneof,
2508                &mut sections.view_oneof,
2509                out,
2510            )?;
2511            emit(
2512                ".__ext",
2513                GeneratedFileKind::Ext,
2514                pc.ext,
2515                &mut sections.ext,
2516                out,
2517            )?;
2518        }
2519        sections
2520    };
2521
2522    let reexport_block = surviving_root_reexports(ctx, files, &reg, root_reexports);
2523
2524    out.push(GeneratedFile {
2525        name: if ctx.config.file_per_package {
2526            package_to_filename(current_package)
2527        } else {
2528            package_to_mod_filename(current_package)
2529        },
2530        package: current_package.to_string(),
2531        kind: GeneratedFileKind::PackageMod,
2532        content: generate_package_mod(
2533            ctx,
2534            &sections,
2535            &reg,
2536            &reexport_block,
2537            fds_bytes,
2538            custom_elem_impls,
2539        )?,
2540    });
2541
2542    // Drop the import registry so its bindings can't leak into the next
2543    // package's generation.
2544    ctx.imports_reset();
2545
2546    Ok(())
2547}
2548
2549/// Names occupied at a package's root by real items: top-level messages,
2550/// enums, message nested-types modules (deconflicted name, #135), and the
2551/// `__buffa` sentinel itself.
2552///
2553/// The package root is shared across every `.proto` file in the package, so
2554/// the set is built from *all* of them. File-level extension consts live in
2555/// `__buffa::ext::`, not at the root, so they are re-export *candidates*
2556/// (added by `generate_proto_content`) rather than occupants. Used both to
2557/// filter root re-exports and as the base reserved set for
2558/// `idiomatic_imports` short-name assignment.
2559fn root_occupied_names(
2560    ctx: &context::CodeGenContext,
2561    files: &[&FileDescriptorProto],
2562) -> std::collections::BTreeSet<String> {
2563    let mut occupied = std::collections::BTreeSet::new();
2564    occupied.insert(context::SENTINEL_MOD.to_string());
2565    for file in files {
2566        let package = file.package.as_deref().unwrap_or("");
2567        for m in &file.message_type {
2568            let name = m.name.as_deref().unwrap_or("");
2569            // The declared struct name carries the configured prefix; the
2570            // module name stays proto-derived.
2571            occupied.insert(ctx.config.prefixed_type_name(name));
2572            // The actual module name (deconflicted from sub-packages, #135).
2573            occupied.insert(ctx.nested_module_name(package, name));
2574        }
2575        for e in &file.enum_type {
2576            occupied.insert(
2577                ctx.config
2578                    .prefixed_type_name(e.name.as_deref().unwrap_or("")),
2579            );
2580        }
2581    }
2582    occupied
2583}
2584
2585/// Filter the candidate package-root re-exports against the package's
2586/// existing root namespace and against each other, returning the surviving
2587/// `pub use` lines.
2588///
2589/// The package root is shared across every `.proto` file in the package, so
2590/// the occupied-name set must be built from *all* of them — a top-level
2591/// message named `FooView` declared in `a.proto` would shadow `Foo`'s view
2592/// re-export from `b.proto`.
2593fn surviving_root_reexports(
2594    ctx: &context::CodeGenContext,
2595    files: &[&FileDescriptorProto],
2596    reg: &message::RegistryPaths,
2597    mut candidates: Vec<message::ReexportCandidate>,
2598) -> TokenStream {
2599    use crate::idents::make_field_ident;
2600
2601    let occupied = root_occupied_names(ctx, files);
2602
2603    // `register_types`, when emitted, lives at `__buffa::register_types`.
2604    // `self::` and `#[doc(inline)]` for the same reasons as the view
2605    // re-exports above. Same `any(json, text)` gate as the fn itself.
2606    if ctx.config.emit_register_fn && !reg.is_empty() {
2607        let sentinel = make_field_ident(context::SENTINEL_MOD);
2608        let json_or_text = ctx.config.feature_gates().json_or_text();
2609        candidates.push(message::ReexportCandidate {
2610            name: "register_types".to_string(),
2611            tokens: feature_gates::cfg_block_any(
2612                quote! {
2613                    #[doc(inline)]
2614                    pub use self :: #sentinel :: register_types;
2615                },
2616                &json_or_text,
2617            ),
2618        });
2619    }
2620
2621    message::emit_surviving_reexports(candidates, &occupied)
2622}
2623
2624/// Render the per-package stitcher: owned items at root plus the
2625/// `__buffa::{view,oneof,ext,...}` module wrappers, followed by the
2626/// surviving package-root `pub use` re-exports.
2627fn generate_package_mod(
2628    ctx: &context::CodeGenContext,
2629    sections: &PackageSections,
2630    reg: &message::RegistryPaths,
2631    root_reexports: &TokenStream,
2632    fds_bytes: &[u8],
2633    custom_elem_impls: &TokenStream,
2634) -> Result<String, CodeGenError> {
2635    use crate::idents::make_field_ident;
2636
2637    let owned = &sections.owned;
2638    let view = &sections.view;
2639    let lazy_view = &sections.lazy_view;
2640    let view_oneof = &sections.view_oneof;
2641    let oneof = &sections.oneof;
2642    let ext = &sections.ext;
2643
2644    // Each ancillary module is emitted only when its section has
2645    // content. The natural-path re-exports outside `__buffa` target
2646    // these modules — they are emitted only when their target items
2647    // exist, so the conditions align and re-exports never reference
2648    // a missing module.
2649    let view_oneof_mod = if !view_oneof.is_empty() {
2650        quote! {
2651            pub mod oneof {
2652                #[allow(unused_imports)]
2653                use super::*;
2654                #(#view_oneof)*
2655            }
2656        }
2657    } else {
2658        TokenStream::new()
2659    };
2660
2661    // `view_oneof` is only populated for messages that have oneofs, and
2662    // every message also contributes to `view`, so `!view.is_empty()` is
2663    // sufficient — `view_oneof` non-empty implies `view` non-empty.
2664    debug_assert!(view_oneof.is_empty() || !view.is_empty());
2665    let view_mod = if ctx.config.generate_views && !view.is_empty() {
2666        feature_gates::cfg_block(
2667            quote! {
2668                pub mod view {
2669                    #[allow(unused_imports)]
2670                    use super::*;
2671                    #(#view)*
2672                    #view_oneof_mod
2673                }
2674            },
2675            ctx.config.feature_gates().views,
2676        )
2677    } else {
2678        TokenStream::new()
2679    };
2680
2681    // `lazy_view` is only populated when `view` is (the lazy family is
2682    // generated per-message alongside the eager view).
2683    debug_assert!(lazy_view.is_empty() || !view.is_empty());
2684    let lazy_view_mod = if !lazy_view.is_empty() {
2685        feature_gates::cfg_block(
2686            quote! {
2687                pub mod lazy_view {
2688                    #[allow(unused_imports)]
2689                    use super::*;
2690                    #(#lazy_view)*
2691                }
2692            },
2693            ctx.config.feature_gates().views,
2694        )
2695    } else {
2696        TokenStream::new()
2697    };
2698
2699    let oneof_mod = if !oneof.is_empty() {
2700        quote! {
2701            pub mod oneof {
2702                #[allow(unused_imports)]
2703                use super::*;
2704                #(#oneof)*
2705            }
2706        }
2707    } else {
2708        TokenStream::new()
2709    };
2710
2711    let ext_mod = if !ext.is_empty() {
2712        quote! {
2713            pub mod ext {
2714                #[allow(unused_imports)]
2715                use super::*;
2716                #(#ext)*
2717            }
2718        }
2719    } else {
2720        TokenStream::new()
2721    };
2722
2723    let register_fn = if ctx.config.emit_register_fn && !reg.is_empty() {
2724        let gates = ctx.config.feature_gates();
2725        // When the gated consts (`__*_JSON_ANY` / `__*_TEXT_ANY`) are
2726        // `#[cfg(feature = "...")]`, each registration statement that
2727        // references them gets the same gate. `#[cfg]` on a statement is
2728        // allowed; the call disappears with the const.
2729        let json_regs = reg
2730            .json_any
2731            .iter()
2732            .map(|p| {
2733                feature_gates::cfg_block(quote! { reg.register_json_any(super::#p); }, gates.json)
2734            })
2735            .chain(reg.json_ext.iter().map(|p| {
2736                feature_gates::cfg_block(quote! { reg.register_json_ext(super::#p); }, gates.json)
2737            }));
2738        let text_regs = reg
2739            .text_any
2740            .iter()
2741            .map(|p| {
2742                feature_gates::cfg_block(quote! { reg.register_text_any(super::#p); }, gates.text)
2743            })
2744            .chain(reg.text_ext.iter().map(|p| {
2745                feature_gates::cfg_block(quote! { reg.register_text_ext(super::#p); }, gates.text)
2746            }));
2747        // When gating, a feature subset may leave one bucket of statements
2748        // cfg'd out while the other survives — `reg` is still used. But if
2749        // `register_types` itself is gated on `any(json, text)` (below),
2750        // the only reachable bodies have at least one statement, so `reg`
2751        // can't be unused. Keep `#[allow(unused_variables)]` defensively
2752        // anyway: it's harmless, and the alternative — proving the
2753        // invariant holds across future statement-shape changes — is
2754        // brittle.
2755        let allow_unused = if ctx.config.gate_impls_on_crate_features {
2756            quote! { #[allow(unused_variables)] }
2757        } else {
2758            quote! {}
2759        };
2760        // The fn is useless without at least one of the gated modes that
2761        // populate it — and `::buffa::type_registry::TypeRegistry` may
2762        // become feature-gated in the runtime in a future release. Gate the
2763        // fn on `any(...)` of whichever modes are active so it disappears
2764        // alongside the last entry.
2765        feature_gates::cfg_block_any(
2766            quote! {
2767                /// Register this package's `Any` type entries and extension entries.
2768                #allow_unused
2769                pub fn register_types(reg: &mut ::buffa::type_registry::TypeRegistry) {
2770                    #(#json_regs)*
2771                    #(#text_regs)*
2772                }
2773            },
2774            &gates.json_or_text(),
2775        )
2776    } else {
2777        TokenStream::new()
2778    };
2779
2780    // Reflection: embed the FileDescriptorSet bytes and a lazy pool
2781    // accessor so per-message `Reflectable` impls have a descriptor pool to
2782    // resolve against. Lives inside `__buffa` so the impls can reach it via
2783    // a relative `__buffa::reflect::descriptor_pool()` path. A package-root
2784    // `pub use` re-exports `descriptor_pool` so consumers don't have to
2785    // route through the reserved `__buffa` sentinel.
2786    let (reflect_mod, reflect_reexport) = if ctx.config.generate_reflection {
2787        let gate = ctx.config.feature_gates().reflect;
2788        (
2789            feature_gates::cfg_block(reflect::reflect_pool_module(fds_bytes), gate),
2790            feature_gates::cfg_block(reflect::pool_accessor_reexport(&quote! { __buffa }), gate),
2791        )
2792    } else {
2793        (TokenStream::new(), TokenStream::new())
2794    };
2795
2796    let sentinel = make_field_ident(context::SENTINEL_MOD);
2797    // The whole `pub mod __buffa { ... }` wrapper is itself omitted
2798    // when none of its inner modules or `register_types` exist.
2799    let buffa_mod = if view_mod.is_empty()
2800        && lazy_view_mod.is_empty()
2801        && oneof_mod.is_empty()
2802        && ext_mod.is_empty()
2803        && register_fn.is_empty()
2804        && reflect_mod.is_empty()
2805        && custom_elem_impls.is_empty()
2806    {
2807        TokenStream::new()
2808    } else {
2809        let allow = allow_lints_attr();
2810        quote! {
2811            #allow
2812            pub mod #sentinel {
2813                #[allow(unused_imports)]
2814                use super::*;
2815                #view_mod
2816                #lazy_view_mod
2817                #oneof_mod
2818                #ext_mod
2819                #register_fn
2820                #reflect_mod
2821                #custom_elem_impls
2822            }
2823        }
2824    };
2825
2826    // Idiomatic imports: the `use` block backing the package-root short
2827    // names (empty unless the registry is in its resolution phase). Only
2828    // ever non-empty in file_per_package mode, where this output is the
2829    // whole single-writer package file.
2830    //
2831    // Load-bearing lint coupling: impl bodies still write fully-qualified
2832    // paths (e.g. `::buffa::MessageField<…>`) for types this block also
2833    // imports — exactly what `unused_qualifications` flags. That lint is
2834    // suppressed by the `ALLOW_LINTS` attr the module-tree wrapper carries,
2835    // so generated files must keep their `#[allow]` wrapper when consumed.
2836    let use_block = ctx.imports_use_block();
2837
2838    let tokens = quote! {
2839        #use_block
2840        #(#owned)*
2841        #buffa_mod
2842        #reflect_reexport
2843        #root_reexports
2844    };
2845
2846    format_tokens(tokens, "")
2847}
2848
2849/// Format a token stream into a generated-file string with the standard
2850/// header comment.
2851fn format_tokens(tokens: TokenStream, source: &str) -> Result<String, CodeGenError> {
2852    let syntax_tree =
2853        syn::parse2::<syn::File>(tokens).map_err(|e| CodeGenError::InvalidSyntax(e.to_string()))?;
2854    let formatted = prettyplease::unparse(&syntax_tree);
2855    let source_line = if source.is_empty() {
2856        String::new()
2857    } else {
2858        format!("// source: {source}\n")
2859    };
2860    Ok(format!(
2861        "// @generated by buffa-codegen. DO NOT EDIT.\n{source_line}\n{formatted}"
2862    ))
2863}
2864
2865/// Convert a proto package name to its `.mod.rs` stitcher filename.
2866///
2867/// e.g., `"google.protobuf"` → `"google.protobuf.mod.rs"`. The unnamed
2868/// package uses the [`SENTINEL_MOD`](context::SENTINEL_MOD) name as its
2869/// filename stem — `package __buffa;` is already rejected by
2870/// `validate_file`, so the unnamed-package stitcher cannot
2871/// collide with any real package's.
2872pub fn package_to_mod_filename(package: &str) -> String {
2873    if package.is_empty() {
2874        format!("{}.mod.rs", context::SENTINEL_MOD)
2875    } else {
2876        format!("{package}.mod.rs")
2877    }
2878}
2879
2880/// Convert a proto package name to its [`file_per_package`] output filename.
2881///
2882/// e.g., `"google.protobuf"` → `"google.protobuf.rs"`. The unnamed
2883/// package uses [`SENTINEL_MOD`](context::SENTINEL_MOD) — same
2884/// collision-avoidance as [`package_to_mod_filename`].
2885///
2886/// [`file_per_package`]: CodeGenConfig::file_per_package
2887pub fn package_to_filename(package: &str) -> String {
2888    if package.is_empty() {
2889        format!("{}.rs", context::SENTINEL_MOD)
2890    } else {
2891        format!("{package}.rs")
2892    }
2893}
2894
2895/// Convert a `.proto` file path to its content-file stem.
2896///
2897/// e.g., `"google/protobuf/timestamp.proto"` → `"google.protobuf.timestamp"`.
2898/// Content files append `""`, `".__view"`, `".__oneof"`,
2899/// `".__view_oneof"`, or `".__ext"` plus `".rs"` — emitted only for
2900/// kinds with non-empty content.
2901pub fn proto_path_to_stem(proto_path: &str) -> String {
2902    let without_ext = proto_path.strip_suffix(".proto").unwrap_or(proto_path);
2903    without_ext.replace('/', ".")
2904}
2905
2906/// Merge downstream [`Companion`](GeneratedFileKind::Companion) files into
2907/// the per-package stitcher produced by [`generate`].
2908///
2909/// For each companion file this function locates the
2910/// [`PackageMod`](GeneratedFileKind::PackageMod) entry in `files` with a
2911/// matching package and appends `include!("<name>");` at file scope after
2912/// buffa's own output — at package root, alongside the owned message types,
2913/// not under `__buffa::`. The companion files themselves are appended to
2914/// `files` so that build integrations can write everything to disk in one
2915/// pass.
2916///
2917/// **Call this once per build**; it does not deduplicate, so a second call
2918/// with the same companions emits a second `include!` for each, which fails
2919/// to compile downstream with a duplicate-definition error.
2920///
2921/// `name` must be a bare-sibling filename — the same convention buffa uses
2922/// for its own `include!` calls, so it resolves relative to the stitcher
2923/// without any `OUT_DIR` prefix. Names must not contain `"`, `\`, `/`, or
2924/// newlines (the function `debug_assert!`s this in debug builds), and must
2925/// not collide with any of buffa's own generated filenames for the same
2926/// package (`<stem>.rs`, `<stem>.__view.rs`, etc.) — pick an unused suffix
2927/// such as `<stem>.__myplugin.rs`.
2928///
2929/// Companion files with no matching `PackageMod` (e.g. for a package buffa
2930/// did not generate any output for) are still appended to `files` but no
2931/// `include!` is emitted; the caller is responsible for wiring them up. If
2932/// you don't expect orphans, check that every companion's `package` appears
2933/// in `files` as a `PackageMod` after calling.
2934pub fn apply_companions(files: &mut Vec<GeneratedFile>, companions: Vec<GeneratedFile>) {
2935    for comp in &companions {
2936        debug_assert!(
2937            !comp.name.contains(['"', '\\', '/', '\n']),
2938            "companion file name {:?} contains a character that would break \
2939             the generated include!() literal or its bare-sibling resolution",
2940            comp.name
2941        );
2942        if let Some(pkg_mod) = files
2943            .iter_mut()
2944            .find(|f| f.kind == GeneratedFileKind::PackageMod && f.package == comp.package)
2945        {
2946            pkg_mod
2947                .content
2948                .push_str(&format!("include!(\"{}\");\n", comp.name));
2949        }
2950    }
2951    files.extend(companions);
2952}
2953
2954/// Code generation error.
2955#[derive(Debug, Clone, thiserror::Error)]
2956#[non_exhaustive]
2957pub enum CodeGenError {
2958    /// A required field was absent in a descriptor.
2959    ///
2960    /// The `&'static str` names the missing field for diagnostics.
2961    #[error("missing required descriptor field: {0}")]
2962    MissingField(&'static str),
2963    /// A resolved type path string could not be parsed as a Rust type.
2964    #[error("invalid Rust type path: '{0}'")]
2965    InvalidTypePath(String),
2966    /// A `box_type_custom` pointer template did not contain the `*` placeholder.
2967    ///
2968    /// The custom pointer wraps the message type, so the template must mark where
2969    /// it goes with `*`, e.g. `"::smallbox::SmallBox<*, smallbox::space::S4>"`.
2970    #[error("box_type template must contain a `*` placeholder for the message type: '{0}'")]
2971    MissingWildcard(String),
2972    /// A `repeated_type_custom` collection template did not contain the `*`
2973    /// element placeholder.
2974    ///
2975    /// Unlike the scalar `string_type_custom` / `bytes_type_custom` knobs (which
2976    /// take a complete type path), a collection template wraps the element type
2977    /// and must mark where it goes with `*`, e.g. `"::my_crate::SmallList<*>"`.
2978    #[error("repeated_type template must contain a `*` element placeholder: '{0}'")]
2979    MissingListPlaceholder(String),
2980    /// The accumulated `TokenStream` failed to parse as valid Rust syntax.
2981    #[error("generated code failed to parse as Rust: {0}")]
2982    InvalidSyntax(String),
2983    /// A requested file was not present in the descriptor set.
2984    #[error("file_to_generate '{0}' not found in descriptor set")]
2985    FileNotFound(String),
2986    /// Unexpected descriptor state (e.g. a map entry or oneof that cannot be
2987    /// resolved to a known descriptor field).
2988    #[error("codegen error: {0}")]
2989    Other(String),
2990    /// A proto field name uses the `__buffa_` reserved prefix, which would
2991    /// conflict with buffa's internal generated fields.
2992    #[error(
2993        "reserved field name '{field_name}' in message '{message_name}': \
2994             proto field names starting with '__buffa_' conflict with buffa's \
2995             internal fields"
2996    )]
2997    ReservedFieldName {
2998        message_name: String,
2999        field_name: String,
3000    },
3001    /// Two sibling messages produce the same Rust module name after
3002    /// snake_case conversion (e.g., `HTTPRequest` and `HttpRequest` both
3003    /// become `pub mod http_request`).
3004    #[error(
3005        "module name conflict in '{scope}': messages '{name_a}' and '{name_b}' \
3006         both produce module '{module_name}'"
3007    )]
3008    ModuleNameConflict {
3009        scope: String,
3010        name_a: String,
3011        name_b: String,
3012        module_name: String,
3013    },
3014    /// A proto package segment, message name, or file-level enum name
3015    /// would emit a Rust item matching the reserved sentinel `__buffa`.
3016    ///
3017    /// This is the only name buffa reserves in user namespace. Resolve by
3018    /// renaming the proto element.
3019    #[error(
3020        "reserved name '{name}' at {location}: this name is reserved for \
3021         buffa's generated ancillary types (views, oneof enums, \
3022         extensions). Rename the proto element."
3023    )]
3024    ReservedModuleName { name: String, location: String },
3025    /// The input contains a message with `option message_set_wire_format = true`
3026    /// but [`CodeGenConfig::allow_message_set`] was not set.
3027    #[error(
3028        "message '{message_name}' uses `option message_set_wire_format = true` \
3029         but CodeGenConfig::allow_message_set is false; MessageSet is a legacy \
3030         wire format — set allow_message_set(true) if this is intentional"
3031    )]
3032    MessageSetNotSupported { message_name: String },
3033    /// A custom attribute string configured via [`CodeGenConfig::type_attributes`],
3034    /// [`CodeGenConfig::field_attributes`], [`CodeGenConfig::message_attributes`],
3035    /// [`CodeGenConfig::enum_attributes`], or [`CodeGenConfig::oneof_attributes`]
3036    /// could not be parsed as a Rust attribute.
3037    #[error(
3038        "invalid custom attribute for path '{path}': '{attribute}' is not a valid \
3039         Rust attribute ({detail})"
3040    )]
3041    InvalidCustomAttribute {
3042        path: String,
3043        attribute: String,
3044        detail: String,
3045    },
3046    /// [`CodeGenConfig::type_name_prefix`] is not PascalCase
3047    /// (`[A-Z][A-Za-z0-9]*`), so prepending it to a type name would produce
3048    /// an invalid or unconventionally-cased Rust identifier.
3049    #[error(
3050        "invalid type_name_prefix '{prefix}': must be empty or PascalCase \
3051         (start with an ASCII uppercase letter, followed by ASCII letters \
3052         and digits only)"
3053    )]
3054    InvalidTypeNamePrefix { prefix: String },
3055}
3056
3057#[cfg(test)]
3058mod tests;