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