Skip to main content

buffa_build/
lib.rs

1//! Build-time integration for buffa.
2//!
3//! Use this crate in your `build.rs` to compile `.proto` files into Rust code
4//! at build time. Parses `.proto` files into a `FileDescriptorSet` (via
5//! `protoc` or `buf`), then uses `buffa-codegen` to generate Rust source.
6//!
7//! # Example
8//!
9//! ```rust,ignore
10//! // build.rs
11//! fn main() {
12//!     buffa_build::Config::new()
13//!         .files(&["proto/my_service.proto"])
14//!         .includes(&["proto/"])
15//!         .compile()
16//!         .unwrap();
17//! }
18//! ```
19//!
20//! # Requirements
21//!
22//! By default, requires `protoc` on the system PATH (or set via the `PROTOC`
23//! environment variable) — the same as `prost-build` and `tonic-build`.
24//!
25//! If `protoc` is unavailable or outdated on your platform, `buf` can be
26//! used instead — see [`Config::use_buf()`]. Alternatively, feed a
27//! pre-compiled descriptor set via [`Config::descriptor_set()`].
28
29use std::path::{Path, PathBuf};
30use std::process::Command;
31
32use buffa::Message;
33use buffa_codegen::generated::descriptor::FileDescriptorSet;
34
35#[doc(inline)]
36pub use buffa_codegen::CodeGenConfig;
37#[doc(inline)]
38pub use buffa_codegen::FeatureGateNames;
39#[doc(inline)]
40pub use buffa_codegen::ReflectMode;
41#[doc(inline)]
42pub use buffa_codegen::{BytesRepr, MapRepr, PointerRepr, RepeatedRepr, StringRepr};
43
44/// How to produce a `FileDescriptorSet` from `.proto` files.
45#[derive(Debug, Clone, Default)]
46enum DescriptorSource {
47    /// Invoke `protoc` (default). Requires `protoc` on PATH or `PROTOC` env var.
48    #[default]
49    Protoc,
50    /// Invoke `buf build --as-file-descriptor-set`. Requires `buf` on PATH.
51    Buf,
52    /// Read a pre-built `FileDescriptorSet` from a file.
53    Precompiled(PathBuf),
54}
55
56/// Builder for configuring and running protobuf compilation.
57pub struct Config {
58    files: Vec<PathBuf>,
59    includes: Vec<PathBuf>,
60    out_dir: Option<PathBuf>,
61    codegen_config: CodeGenConfig,
62    descriptor_source: DescriptorSource,
63    /// If set, generate a module-tree include file with this name in the
64    /// output directory. Users can then `include!` this single file instead
65    /// of manually setting up `pub mod` nesting.
66    include_file: Option<String>,
67}
68
69impl Config {
70    /// Create a new configuration with defaults.
71    pub fn new() -> Self {
72        Self {
73            files: Vec::new(),
74            includes: Vec::new(),
75            out_dir: None,
76            codegen_config: CodeGenConfig::default(),
77            descriptor_source: DescriptorSource::default(),
78            include_file: None,
79        }
80    }
81
82    /// Add `.proto` files to compile.
83    #[must_use]
84    pub fn files(mut self, files: &[impl AsRef<Path>]) -> Self {
85        self.files
86            .extend(files.iter().map(|f| f.as_ref().to_path_buf()));
87        self
88    }
89
90    /// Add include directories for protoc to search for imports.
91    #[must_use]
92    pub fn includes(mut self, includes: &[impl AsRef<Path>]) -> Self {
93        self.includes
94            .extend(includes.iter().map(|i| i.as_ref().to_path_buf()));
95        self
96    }
97
98    /// Set the output directory for generated files.
99    /// Defaults to `$OUT_DIR` if not set.
100    #[must_use]
101    pub fn out_dir(mut self, dir: impl Into<PathBuf>) -> Self {
102        self.out_dir = Some(dir.into());
103        self
104    }
105
106    /// Enable or disable view type generation (default: true).
107    #[must_use]
108    pub fn generate_views(mut self, enabled: bool) -> Self {
109        self.codegen_config.generate_views = enabled;
110        self
111    }
112
113    /// Additionally generate the lazy view family (`FooLazyView<'a>`)
114    /// alongside the unchanged eager views (default: false).
115    ///
116    /// Lazy views decode in a single non-recursive pass, recording nested and
117    /// repeated message fields as undecoded byte ranges that decode on access
118    /// via fallible, by-value accessors (`.get()` / iteration) — untouched
119    /// sub-trees cost nothing. Validation of deferred bytes happens on
120    /// *access* (and in the fallible `to_owned_message`), not at decode.
121    /// Groups, oneof message variants, and map message values stay eager;
122    /// lazy views have no `ReflectMessage`/`OwnedView`/text surface. Eager
123    /// codegen output is byte-identical with or without the flag. Requires
124    /// [`generate_views`](Self::generate_views). See
125    /// [`CodeGenConfig::lazy_views`] for full semantics.
126    #[must_use]
127    pub fn lazy_views(mut self, enabled: bool) -> Self {
128        self.codegen_config.lazy_views = enabled;
129        self
130    }
131
132    /// Enable or disable serde JSON generation (default: false).
133    ///
134    /// When enabled:
135    /// - Generated message structs get `Serialize`/`Deserialize` derives.
136    /// - Generated enum types get `Serialize`/`Deserialize` derives.
137    /// - Generated view types (when `generate_views` is also enabled) get a
138    ///   manual `impl Serialize` for zero-copy JSON serialization, so
139    ///   `serde_json::to_string(&view)` works directly:
140    ///
141    ///   ```ignore
142    ///   let view = MyMsgView::decode_view(&bytes)?;
143    ///   let json = serde_json::to_string(&view)?;
144    ///   ```
145    ///
146    /// The downstream crate must depend on `serde` and enable the `buffa/json`
147    /// feature for the runtime helpers. When views are enabled, the crate must
148    /// also enable `buffa-types/json` so the well-known type views implement
149    /// `Serialize`; without it, references to e.g. `TimestampView<'_>` in the
150    /// generated `Serialize` impl will fail with
151    /// `the trait bound 'TimestampView<'_>: Serialize' is not satisfied`.
152    ///
153    /// **Limitations of the view `Serialize` impl:**
154    /// - Extension fields are not included in view JSON output; serialize the
155    ///   owned form (`view.to_owned_message()`) to include extensions.
156    /// - The impl uses `serialize_map(None)` (unknown length) because the
157    ///   number of emitted fields depends on default-omission rules. Most
158    ///   self-describing serializers (notably `serde_json`) accept this, but
159    ///   length-prefixed formats (e.g. `bincode`, `postcard`) will return a
160    ///   runtime error. The owned types' derived `Serialize` does not have this
161    ///   restriction.
162    #[must_use]
163    pub fn generate_json(mut self, enabled: bool) -> Self {
164        self.codegen_config.generate_json = enabled;
165        self
166    }
167
168    /// Enable or disable `impl buffa::text::TextFormat` on generated message
169    /// structs (default: false).
170    ///
171    /// When enabled, the downstream crate must enable the `buffa/text`
172    /// feature for the runtime textproto encoder/decoder.
173    #[must_use]
174    pub fn generate_text(mut self, enabled: bool) -> Self {
175        self.codegen_config.generate_text = enabled;
176        self
177    }
178
179    /// Enable or disable `#[derive(arbitrary::Arbitrary)]` on generated
180    /// types (default: false).
181    ///
182    /// The derive is gated behind `#[cfg_attr(feature = "arbitrary", ...)]`
183    /// so the downstream crate compiles with or without the feature enabled.
184    ///
185    /// Your crate's Cargo feature **must be named exactly `"arbitrary"`** —
186    /// the generated `cfg_attr` uses that literal string and cannot be
187    /// customised — and it must forward to `buffa/arbitrary`:
188    ///
189    /// ```toml
190    /// [features]
191    /// arbitrary = ["dep:arbitrary", "buffa/arbitrary"]
192    /// ```
193    ///
194    /// Forgetting `"buffa/arbitrary"` produces a confusing
195    /// `cannot find function 'arbitrary_bytes' in module '__private'` error
196    /// in generated code when [`use_bytes_type`](Self::use_bytes_type) or
197    /// [`use_bytes_type_in`](Self::use_bytes_type_in) is also enabled,
198    /// because the helper that backs `#[arbitrary(with = ...)]` for
199    /// `bytes::Bytes` fields lives in `buffa` under that feature gate.
200    #[must_use]
201    pub fn generate_arbitrary(mut self, enabled: bool) -> Self {
202        self.codegen_config.generate_arbitrary = enabled;
203        self
204    }
205
206    /// Wrap generated `impl`s in `#[cfg(feature = "...")]` instead of
207    /// emitting them unconditionally (default: false).
208    ///
209    /// When enabled, the impls controlled by [`generate_json`],
210    /// [`generate_views`], and [`generate_text`] are wrapped in
211    /// `#[cfg(feature = "json" | "views" | "text")]` (or
212    /// `#[cfg_attr(feature = ..., ...)]` for derives and field attributes)
213    /// rather than emitted unconditionally. The crate consuming the
214    /// generated code must define matching Cargo features that enable the
215    /// corresponding runtime support:
216    ///
217    /// ```toml
218    /// [features]
219    /// json  = ["buffa/json", "dep:serde", "dep:serde_json"]
220    /// views = []
221    /// text  = ["buffa/text"]
222    /// ```
223    ///
224    /// The `generate_*` flags still control *whether* an impl kind is
225    /// emitted at all — this flag only controls whether it is `cfg`-gated.
226    /// `generate_arbitrary` is always `cfg_attr`-gated on
227    /// `feature = "arbitrary"` regardless of this flag, because `arbitrary`
228    /// is an optional dependency by design.
229    ///
230    /// Reach for this when generated code is the **public interface of a
231    /// library crate** consumed by downstream projects with different
232    /// feature needs — exactly the shape of `buffa-descriptor` and
233    /// `buffa-types`, which ship every impl while letting the codegen
234    /// toolchain (`buffa-codegen`/`buffa-build`/`protoc-gen-buffa`) depend
235    /// on them with `default-features = false` and stay free of
236    /// `serde`/`serde_json`/`base64`. Most consumers of `buffa-build` are
237    /// **not** in this position: a `build.rs` that decides at build-script
238    /// time whether to generate JSON wants `impl Serialize` to just exist.
239    /// Default `false`.
240    ///
241    /// [`generate_json`]: Self::generate_json
242    /// [`generate_views`]: Self::generate_views
243    /// [`generate_text`]: Self::generate_text
244    #[must_use]
245    pub fn gate_impls_on_crate_features(mut self, enabled: bool) -> Self {
246        self.codegen_config.gate_impls_on_crate_features = enabled;
247        self
248    }
249
250    /// Gate only the reflection impls behind a `reflect` crate feature, without
251    /// gating json/views/text (unlike
252    /// [`gate_impls_on_crate_features`](Self::gate_impls_on_crate_features),
253    /// which gates them together).
254    ///
255    /// For crates that ship views/text unconditionally but want the
256    /// `buffa-descriptor`-dependent (and `std`-requiring) reflection surface to
257    /// be opt-in. `buffa-types` is the motivating case.
258    ///
259    /// **Experimental and `#[doc(hidden)]`**, paired with
260    /// [`generate_reflection_vtable`](Self::generate_reflection_vtable) until the
261    /// public `ReflectMode` selector lands.
262    #[doc(hidden)]
263    #[must_use]
264    pub fn gate_reflect_on_crate_feature(mut self, enabled: bool) -> Self {
265        self.codegen_config.gate_reflect_on_crate_feature = enabled;
266        self
267    }
268
269    /// Set the crate feature name the gated JSON impls are conditioned on
270    /// (default: `"json"`).
271    ///
272    /// Only meaningful together with
273    /// [`gate_impls_on_crate_features`](Self::gate_impls_on_crate_features);
274    /// inert otherwise. Use when the consuming crate gates its JSON support
275    /// behind a differently-named feature:
276    ///
277    /// ```toml
278    /// [features]
279    /// serde = ["buffa/json", "dep:serde", "dep:serde_json"]
280    /// ```
281    ///
282    /// ```rust,ignore
283    /// buffa_build::Config::new()
284    ///     .generate_json(true)
285    ///     .gate_impls_on_crate_features(true)
286    ///     .json_feature_name("serde")
287    /// # ;
288    /// ```
289    ///
290    /// The name is emitted verbatim into `#[cfg(feature = "...")]`
291    /// attributes and must be a valid Cargo feature name **declared in the
292    /// consuming crate's `[features]` table**. A misspelled or undeclared
293    /// name fails open: the `#[cfg]` is permanently false, so the gated
294    /// impls silently compile away (on Rust ≥ 1.80 an undeclared name at
295    /// least triggers the `unexpected_cfgs` warning). A name that is not a
296    /// valid Cargo feature name at all (empty, or containing characters
297    /// outside alphanumerics and `_`/`-`/`+`/`.`) makes [`compile`](Self::compile)
298    /// fail with an error when the gate is active.
299    #[must_use]
300    pub fn json_feature_name(mut self, name: impl Into<String>) -> Self {
301        self.codegen_config.feature_gate_names.json = name.into();
302        self
303    }
304
305    /// Set the crate feature name the gated view impls are conditioned on
306    /// (default: `"views"`).
307    ///
308    /// Only meaningful together with
309    /// [`gate_impls_on_crate_features`](Self::gate_impls_on_crate_features);
310    /// inert otherwise. See [`json_feature_name`](Self::json_feature_name).
311    #[must_use]
312    pub fn views_feature_name(mut self, name: impl Into<String>) -> Self {
313        self.codegen_config.feature_gate_names.views = name.into();
314        self
315    }
316
317    /// Set the crate feature name the gated textproto impls are conditioned
318    /// on (default: `"text"`).
319    ///
320    /// Only meaningful together with
321    /// [`gate_impls_on_crate_features`](Self::gate_impls_on_crate_features);
322    /// inert otherwise. See [`json_feature_name`](Self::json_feature_name).
323    #[must_use]
324    pub fn text_feature_name(mut self, name: impl Into<String>) -> Self {
325        self.codegen_config.feature_gate_names.text = name.into();
326        self
327    }
328
329    /// Set the crate feature name the gated reflection impls are conditioned
330    /// on (default: `"reflect"`).
331    ///
332    /// Only meaningful together with
333    /// [`gate_impls_on_crate_features`](Self::gate_impls_on_crate_features)
334    /// (or the experimental, hidden `gate_reflect_on_crate_feature`, which
335    /// gates reflection alone); inert otherwise. See
336    /// [`json_feature_name`](Self::json_feature_name).
337    #[must_use]
338    pub fn reflect_feature_name(mut self, name: impl Into<String>) -> Self {
339        self.codegen_config.feature_gate_names.reflect = name.into();
340        self
341    }
342
343    /// Prepend a prefix to every generated Rust type name (default: none).
344    ///
345    /// With prefix `"Rpc"`, `message User {}` generates `struct RpcUser`
346    /// (and `RpcUserView` / `RpcUserOwnedView`); every cross-reference uses
347    /// the prefixed name. Useful in multi-protocol systems where generated
348    /// types from different domains would otherwise collide with each other
349    /// or with a canonical hand-written model.
350    ///
351    /// Applies to message structs and enum types (top-level and nested).
352    /// Module names, oneof enums, [`extern_path`](Self::extern_path)-mapped
353    /// types (including well-known types), and the wire/JSON format are
354    /// unaffected.
355    ///
356    /// When another crate references these prefixed types via its own
357    /// [`extern_path`](Self::extern_path) mapping, the mapped Rust path must
358    /// spell out the prefixed name (e.g. `::crate_a::RpcUser`) — the proto
359    /// name carries no prefix, so the mapping is not derived automatically.
360    ///
361    /// The prefix must be PascalCase (`[A-Z][A-Za-z0-9]*`) — an ASCII
362    /// uppercase letter followed by ASCII letters and digits — so the
363    /// prefixed names stay conventionally cased; [`compile`](Self::compile)
364    /// fails otherwise.
365    #[must_use]
366    pub fn type_name_prefix(mut self, prefix: impl Into<String>) -> Self {
367        self.codegen_config.type_name_prefix = prefix.into();
368        self
369    }
370
371    /// Enable or disable `with_*` builder-style setter methods for
372    /// explicit-presence fields (default: true).
373    ///
374    /// Each explicit-presence scalar, bytes, or enum field gets a
375    /// `pub fn with_<name>(mut self, value: T) -> Self` method that wraps the
376    /// value in `Some(...)` and returns `self`, enabling chained construction
377    /// without the `Some(...)` boilerplate:
378    ///
379    /// ```ignore
380    /// let req = MyRequest::default()
381    ///     .with_name("alice")
382    ///     .with_timeout_ms(30_000);
383    /// ```
384    ///
385    /// String, bytes, and enum setters take `impl Into<T>` (so `&str`,
386    /// `b"..."` literals, and bare enum variants work directly); other
387    /// scalars take `T` to keep integer-literal inference unambiguous.
388    ///
389    /// Setters are pure inherent methods with no runtime dependency — they
390    /// don't interact with the `json`/`views`/`text` feature gates. Disable
391    /// only if you want to keep generated code minimal or have a competing
392    /// `with_*` convention in your own crate.
393    #[must_use]
394    pub fn generate_with_setters(mut self, enabled: bool) -> Self {
395        self.codegen_config.generate_with_setters = enabled;
396        self
397    }
398
399    /// Enable reflection on generated types (default: off).
400    ///
401    /// `generate_reflection(true)` selects [`ReflectMode::VTable`] — the fast
402    /// path: `foo.reflect()` borrows `foo` directly (no encode/decode
403    /// round-trip), and owned and view types implement `ReflectMessage`. For
404    /// the smaller bridge implementation (`reflect()` round-trips through a
405    /// [`DynamicMessage`]), use [`reflect_mode(ReflectMode::Bridge)`](Self::reflect_mode)
406    /// instead. `generate_reflection(false)` is [`ReflectMode::Off`].
407    ///
408    /// Either mode embeds a lazily-built [`DescriptorPool`] (as
409    /// `FileDescriptorSet` bytes) reachable as
410    /// `your_crate::your_pkg::descriptor_pool()`.
411    ///
412    /// # Cargo.toml setup
413    ///
414    /// The consuming crate must depend on `buffa-descriptor` with the
415    /// `reflect` feature and on `std`:
416    ///
417    /// ```toml
418    /// [dependencies]
419    /// buffa = { version = "0.7", features = ["std"] }
420    /// buffa-descriptor = { version = "0.7", features = ["reflect", "std"] }
421    /// ```
422    ///
423    /// When [`gate_impls_on_crate_features`](Self::gate_impls_on_crate_features)
424    /// is also on, the impls are wrapped in `#[cfg(feature = "reflect")]`,
425    /// so the consuming crate must declare a forwarding feature:
426    ///
427    /// ```toml
428    /// [features]
429    /// reflect = ["buffa-descriptor/reflect"]
430    /// ```
431    ///
432    /// **Without the feature declared, the generated `Reflectable` impls
433    /// silently disappear** — `cfg(feature = "reflect")` is permanently
434    /// false in a crate that doesn't declare it. The first call to
435    /// `.reflect()` fails to compile with "trait `Reflectable` not
436    /// implemented", which is a misleading diagnostic. Most consumers
437    /// should leave `gate_impls_on_crate_features` off.
438    ///
439    /// Reflecting message-typed fields also requires every crate that field
440    /// types resolve to via an extern path — notably `buffa-types` for
441    /// well-known types — to enable its own reflection feature; see
442    /// [`reflect_mode`](Self::reflect_mode) ("Extern-path types") for the
443    /// `Cargo.toml` requirement and mixed-mode behavior.
444    ///
445    /// # Performance
446    ///
447    /// In the default vtable mode, `reflect()` borrows `self` — no round-trip,
448    /// no allocation; reflective accessors read fields in place. (Bridge mode
449    /// instead pays one encode/decode round-trip plus a heap allocation per
450    /// call.) Either way the first call pays a one-time pool build cost.
451    ///
452    /// # Build time and binary size
453    ///
454    /// Each generated package embeds its own copy of the full
455    /// `FileDescriptorSet` (transitive closure). For a single-package
456    /// crate this is one copy. For a multi-package codegen run the bytes
457    /// duplicate per package — measurable for large proto trees. The
458    /// serialization happens once per `compile()` call (not per package),
459    /// so build-time CPU does not scale with package count. Vtable mode also
460    /// emits an `impl ReflectMessage` per type, so it produces more code than
461    /// bridge mode.
462    ///
463    /// [`ReflectCow`]: https://docs.rs/buffa-descriptor/latest/buffa_descriptor/reflect/enum.ReflectCow.html
464    /// [`DynamicMessage`]: https://docs.rs/buffa-descriptor/latest/buffa_descriptor/reflect/struct.DynamicMessage.html
465    /// [`DescriptorPool`]: https://docs.rs/buffa-descriptor/latest/buffa_descriptor/struct.DescriptorPool.html
466    #[must_use]
467    pub fn generate_reflection(mut self, enabled: bool) -> Self {
468        // The simple on/off knob selects the fast vtable path; Bridge is opt-in
469        // via `reflect_mode`.
470        let mode = if enabled {
471            ReflectMode::VTable
472        } else {
473            ReflectMode::Off
474        };
475        mode.apply(&mut self.codegen_config);
476        self
477    }
478
479    /// Select the reflection mode (the fuller form of
480    /// [`generate_reflection`](Self::generate_reflection)).
481    ///
482    /// - [`ReflectMode::Off`] — no reflection (the default); equivalent to
483    ///   `generate_reflection(false)`.
484    /// - [`ReflectMode::Bridge`] — `reflect()` round-trips through
485    ///   `DynamicMessage`; smaller generated code, slower reflective access.
486    /// - [`ReflectMode::VTable`] — `impl ReflectMessage` on owned and view
487    ///   types, and `reflect()` borrows `self` with no round-trip; equivalent
488    ///   to `generate_reflection(true)`. Does not require view generation —
489    ///   with views off, only the owned impls are emitted.
490    ///
491    /// All non-`Off` modes require the consuming crate to depend on
492    /// `buffa-descriptor` with its `reflect` feature and on `std`. The call
493    /// site (`foo.reflect().get(fd)`) is identical across modes.
494    ///
495    /// # Extern-path types
496    ///
497    /// Reflection on a message reaches into its message-typed fields, so
498    /// every crate that field types resolve to via an extern path must have
499    /// its own reflection enabled. In particular, well-known types resolve
500    /// to `buffa-types` by default, and its impls are behind a cargo
501    /// feature: depend on `buffa-types = { ..., features = ["reflect"] }`
502    /// or the build fails with unsatisfied `Reflectable` /
503    /// `ReflectMessage` bounds on the WKT.
504    ///
505    /// # Mixed modes
506    ///
507    /// A vtable-mode message may embed owned message types generated in
508    /// bridge mode (e.g. a dependency crate that chose the smaller output):
509    /// reflective access degrades to an owned `DynamicMessage` snapshot at
510    /// that boundary instead of failing. For a bridge-grade `repeated` or
511    /// `map` field the snapshot is taken per element on every access, so
512    /// reflecting a large mixed-mode collection scales the encode/decode
513    /// cost by the element count. The *view* reflection surface cannot
514    /// degrade — every view type embedded in a vtable-mode view must itself
515    /// be vtable-grade, and a bridge-grade view field is a compile error.
516    #[must_use]
517    pub fn reflect_mode(mut self, mode: ReflectMode) -> Self {
518        mode.apply(&mut self.codegen_config);
519        self
520    }
521
522    /// Enable or disable idiomatic `UpperCamelCase` enum aliases (matches the
523    /// [`CodeGenConfig`] default, currently on).
524    ///
525    /// Protobuf enum values are `SHOUTY_SNAKE_CASE` and stay the definitive Rust
526    /// variants. When enabled, codegen additionally emits associated `const`s
527    /// with the enum-name prefix stripped and the name converted to
528    /// `UpperCamelCase` (`RULE_LEVEL_HIGH` → `RuleLevel::High`), purely
529    /// additively — existing references and `Debug` output are unchanged.
530    ///
531    /// Aliases are suppressed per enum (with a build warning and a doc note) if
532    /// any two values would collide after conversion, so a match is never forced
533    /// to mix conventions. See [`CodeGenConfig::idiomatic_enum_aliases`].
534    #[must_use]
535    pub fn idiomatic_enum_aliases(mut self, enabled: bool) -> Self {
536        self.codegen_config.idiomatic_enum_aliases = enabled;
537        self
538    }
539
540    /// Emit one `<dotted.package>.rs` file per proto package instead of the
541    /// per-proto-file content set plus `<pkg>.mod.rs` stitcher. Default:
542    /// `false`.
543    ///
544    /// The single file inlines what the stitcher would otherwise `include!`,
545    /// producing the same module structure. Required by
546    /// [`idiomatic_imports`](Self::idiomatic_imports). See
547    /// [`CodeGenConfig::file_per_package`] for caveats about packages that
548    /// span multiple directories.
549    #[must_use]
550    pub fn file_per_package(mut self, enabled: bool) -> Self {
551        self.codegen_config.file_per_package = enabled;
552        self
553    }
554
555    /// **Experimental.** Emit `use`-backed short type names at the package
556    /// root instead of fully-qualified paths, so struct fields read
557    /// `MessageField<Timestamp>` instead of
558    /// `::buffa::MessageField<::buffa_types::google::protobuf::Timestamp>`.
559    /// Default: `false` (output is byte-for-byte identical to previous
560    /// releases).
561    ///
562    /// Requires [`file_per_package`](Self::file_per_package) — the build
563    /// fails otherwise. Short names that would collide with another item at
564    /// the package root (or a name referenced bare by sibling emissions)
565    /// fall back to parent-module qualification, then to the
566    /// fully-qualified path.
567    ///
568    /// Only package-root type *declarations* are shortened; impl bodies,
569    /// nested-message modules, and `__buffa` internals keep fully-qualified
570    /// paths. "Experimental" means the output shape may change between
571    /// releases and the option may be renamed or removed outside semver
572    /// guarantees. See [`CodeGenConfig::idiomatic_imports`] for details.
573    #[must_use]
574    pub fn idiomatic_imports(mut self, enabled: bool) -> Self {
575        self.codegen_config.idiomatic_imports = enabled;
576        self
577    }
578
579    /// Enable or disable unknown field preservation (default: true).
580    ///
581    /// When enabled (the default), unrecognized fields encountered during
582    /// decode are stored and re-emitted on encode — essential for proxy /
583    /// middleware services and round-trip fidelity across schema versions.
584    ///
585    /// **Disabling is primarily a memory optimization** (24 bytes/message for
586    /// the `UnknownFields` Vec header), not a throughput one. When no unknown
587    /// fields appear on the wire — the common case for schema-aligned
588    /// services — decode and encode costs are effectively identical in
589    /// either mode. Consider disabling for embedded / `no_std` targets or
590    /// large in-memory collections of small messages.
591    #[must_use]
592    pub fn preserve_unknown_fields(mut self, enabled: bool) -> Self {
593        self.codegen_config.preserve_unknown_fields = enabled;
594        self
595    }
596
597    /// Honor `features.utf8_validation = NONE` by emitting `Vec<u8>` / `&[u8]`
598    /// for such string fields instead of `String` / `&str` (default: false).
599    ///
600    /// When disabled (the default), all string fields map to `String` and
601    /// UTF-8 is validated on decode — stricter than proto2 requires, but
602    /// ergonomic and safe.
603    ///
604    /// When enabled, string fields with `utf8_validation = NONE` become
605    /// `Vec<u8>` / `&[u8]`. Decode skips validation; the caller chooses
606    /// whether to `std::str::from_utf8` (checked) or `from_utf8_unchecked`
607    /// (trusted-input fast path). This is the only sound Rust mapping when
608    /// strings may actually contain non-UTF-8 bytes.
609    ///
610    /// **Note for proto2 users**: proto2's default is `utf8_validation = NONE`,
611    /// so enabling this turns ALL proto2 string fields into `Vec<u8>`. Use
612    /// only for new code or when profiling identifies UTF-8 validation as a
613    /// bottleneck (it can be 10%+ of decode CPU for string-heavy messages).
614    ///
615    /// **JSON note**: fields normalized to bytes serialize as base64 in JSON
616    /// (the proto3 JSON encoding for `bytes`). Keep strict mapping disabled
617    /// for fields that need JSON string interop with other implementations.
618    ///
619    /// **Interaction with [`use_bytes_type`]**: when both are enabled,
620    /// `map<bytes, bytes>` values stay `Vec<u8>` (the bytes-keyed JSON helper
621    /// is concrete `HashMap<Vec<u8>, Vec<u8>>`). All other `bytes` shapes —
622    /// singular / optional / repeated / oneof / `map<non-bytes, bytes>` —
623    /// still become `bytes::Bytes`. The asymmetry is documented; if you hit
624    /// it, see issue #76.
625    ///
626    /// [`use_bytes_type`]: Self::use_bytes_type
627    #[must_use]
628    pub fn strict_utf8_mapping(mut self, enabled: bool) -> Self {
629        self.codegen_config.strict_utf8_mapping = enabled;
630        self
631    }
632
633    /// Permit `option message_set_wire_format = true` on input messages.
634    ///
635    /// MessageSet is a legacy Google-internal wire format. Default: `false`
636    /// (such messages produce a codegen error). Set to `true` only when
637    /// compiling protos that interoperate with old Google-internal services.
638    #[must_use]
639    pub fn allow_message_set(mut self, enabled: bool) -> Self {
640        self.codegen_config.allow_message_set = enabled;
641        self
642    }
643
644    /// Declare an external type path mapping.
645    ///
646    /// The matched types reference the specified Rust path instead of being
647    /// generated. This allows shared proto packages to be compiled once in a
648    /// dedicated crate and referenced from others.
649    ///
650    /// `proto_path` is a fully-qualified protobuf path — either a **package**
651    /// (`".my.common"`, mapping every type under it to a Rust module root) or a
652    /// single **type FQN** (`".google.protobuf.Timestamp"`, mapping just that
653    /// type, the prost/tonic idiom). The leading dot is optional and is added
654    /// automatically. As in prost, the most specific entry wins: an exact type
655    /// FQN beats a covering package prefix, which in turn beats a shorter
656    /// prefix.
657    ///
658    /// `rust_path` is where the type(s) are accessible — a module root for a
659    /// package mapping (e.g. `"::common_protos"`) or a full type path for a
660    /// per-type mapping (e.g. `"::pbjson_types::Timestamp"`). It must be an
661    /// absolute path (starting with `::` or `crate::`); any other value is
662    /// emitted into the generated code verbatim and will fail to resolve there.
663    ///
664    /// **Nested types** inherit an enclosing message's per-type override:
665    /// mapping `.my.pkg.Outer` to `::ext::Outer` resolves `.my.pkg.Outer.Inner`
666    /// to `::ext::outer::Inner` — the override's parent module plus buffa's
667    /// usual `snake_case(MessageName)` nested-types module (snake case of the
668    /// *proto* message name, regardless of the override's final segment). This
669    /// matches the layout of another buffa-generated crate; for a target crate
670    /// laid out differently, add explicit per-type entries for the nested types
671    /// as well.
672    ///
673    /// # Limitations
674    ///
675    /// An extern type that is referenced by a generated **view** must map to
676    /// another buffa-generated crate — the view path is composed as
677    /// `<rust_path_root>::__buffa::view::…`, which a non-buffa crate (e.g.
678    /// `pbjson_types`) does not provide. Map per-type to a buffa crate, or
679    /// disable views ([`generate_views(false)`](Self::generate_views)), for
680    /// such types.
681    ///
682    /// A misconfigured mapping (a typo'd FQN target, a non-absolute
683    /// `rust_path`, or a view-referenced type mapped to a non-buffa crate) is
684    /// not diagnosed at generation time; it surfaces as an unresolved-path
685    /// error when the generated code is compiled.
686    ///
687    /// # Example
688    ///
689    /// ```rust,ignore
690    /// buffa_build::Config::new()
691    ///     // Whole-package mapping.
692    ///     .extern_path(".my.common", "::common_protos")
693    ///     // Per-type mapping (issue #111) — overrides the package prefix for
694    ///     // just this type.
695    ///     .extern_path(".google.protobuf.Timestamp", "::common_protos::well_known::Timestamp")
696    ///     .files(&["proto/my_service.proto"])
697    ///     .includes(&["proto/"])
698    ///     .compile()
699    ///     .unwrap();
700    /// ```
701    #[must_use]
702    pub fn extern_path(
703        mut self,
704        proto_path: impl Into<String>,
705        rust_path: impl Into<String>,
706    ) -> Self {
707        let mut proto_path = proto_path.into();
708        // Normalize: ensure the proto path is fully-qualified (leading dot).
709        // Accept both ".my.package" and "my.package" for convenience.
710        if !proto_path.starts_with('.') {
711            proto_path.insert(0, '.');
712        }
713        self.codegen_config
714            .extern_paths
715            .push((proto_path, rust_path.into()));
716        self
717    }
718
719    /// Configure `bytes` fields to use `bytes::Bytes` instead of `Vec<u8>`.
720    ///
721    /// Each path is a fully-qualified proto path prefix. Use `"."` to apply
722    /// to all bytes fields, or specify individual field paths like
723    /// `".my.pkg.MyMessage.data"`.
724    ///
725    /// Applies uniformly to singular, optional, repeated, oneof, **and
726    /// `map<K, bytes>`** values — the map case lets `view → owned`
727    /// conversion participate in the `to_owned_from_source` zero-copy
728    /// `slice_ref` path. One carve-out: an effective `map<bytes, bytes>` keeps
729    /// `Vec<u8>` values (the JSON helper for that combination is concrete
730    /// `HashMap<Vec<u8>, Vec<u8>>`); every other shape becomes `Bytes`. A
731    /// `bytes` map key is only reachable when [`strict_utf8_mapping`] is enabled
732    /// *and* the `map<string, bytes>` field carries
733    /// `[features.utf8_validation = NONE]` on its key, which normalizes the
734    /// string key to `bytes` — `strict_utf8_mapping` alone does not trigger it.
735    ///
736    /// A **custom** `bytes` representation
737    /// ([`bytes_type_custom`](Self::bytes_type_custom)) is honored for
738    /// `map<K, bytes>` values too, the same as the built-in `Bytes` — but a
739    /// custom map value (like a custom `repeated` element) must be a crate-local
740    /// type, since codegen emits its `ReflectElement` / `ProtoElemJson` impls
741    /// (the orphan rule forbids them for a foreign type).
742    ///
743    /// [`strict_utf8_mapping`]: Self::strict_utf8_mapping
744    ///
745    /// # Example
746    ///
747    /// ```rust,ignore
748    /// buffa_build::Config::new()
749    ///     .use_bytes_type_in(&["."])  // all bytes fields use Bytes
750    ///     .files(&["proto/my_service.proto"])
751    ///     .includes(&["proto/"])
752    ///     .compile()
753    ///     .unwrap();
754    /// ```
755    #[must_use]
756    pub fn use_bytes_type_in(self, paths: &[impl AsRef<str>]) -> Self {
757        self.bytes_type_in(BytesRepr::Bytes, paths)
758    }
759
760    /// Use `bytes::Bytes` for all `bytes` fields in all messages.
761    ///
762    /// This is a convenience for `.use_bytes_type_in(&["."])`. Use
763    /// [`use_bytes_type_in`] with specific proto paths if you only want `Bytes`
764    /// for certain fields. See that method for the path-matching semantics, the
765    /// `map<K, bytes>` rule, and the `map<bytes, bytes>` carve-out under
766    /// [`strict_utf8_mapping`].
767    ///
768    /// [`use_bytes_type_in`]: Self::use_bytes_type_in
769    /// [`strict_utf8_mapping`]: Self::strict_utf8_mapping
770    #[must_use]
771    pub fn use_bytes_type(self) -> Self {
772        self.bytes_type(BytesRepr::Bytes)
773    }
774
775    /// Map `bytes` fields to a [`BytesRepr`] other than `Vec<u8>` for the given
776    /// proto path prefixes. The bytes counterpart to
777    /// [`string_type_in`](Self::string_type_in).
778    ///
779    /// Rules accumulate and the **last** matching rule wins, so call the broad
780    /// [`bytes_type`](Self::bytes_type) *first*, then `bytes_type_in` for
781    /// narrower overrides. For [`BytesRepr::Custom`], the downstream crate must
782    /// depend on the crate providing the type (buffa does not re-export it).
783    /// Only the owned Rust type changes — the wire format is unchanged and view
784    /// types still borrow `&[u8]`.
785    #[must_use]
786    pub fn bytes_type_in(mut self, repr: BytesRepr, paths: &[impl AsRef<str>]) -> Self {
787        self.codegen_config
788            .bytes_fields
789            .extend(paths.iter().map(|p| (p.as_ref().to_string(), repr.clone())));
790        self
791    }
792
793    /// Map every `bytes` field in all messages to the given [`BytesRepr`].
794    /// Convenience for `.bytes_type_in(repr, &["."])`; call before any
795    /// [`bytes_type_in`](Self::bytes_type_in) overrides (last matching rule
796    /// wins).
797    #[must_use]
798    pub fn bytes_type(mut self, repr: BytesRepr) -> Self {
799        self.codegen_config
800            .bytes_fields
801            .push((".".to_string(), repr));
802        self
803    }
804
805    /// Map the matching `bytes` fields to a custom type named by its
806    /// fully-qualified Rust path (e.g. `"::my_crate::MyBytes"`). The type must
807    /// satisfy `buffa::ProtoBytes`, and the downstream crate must depend on the
808    /// crate providing it. Shorthand for
809    /// [`bytes_type_in`](Self::bytes_type_in)`(BytesRepr::Custom(path), paths)`.
810    ///
811    /// # Limitations
812    ///
813    /// - A **foreign** custom type used as a `repeated` element — or a
814    ///   `map<K, bytes>` value — fails to compile: codegen emits
815    ///   `ReflectElement` / `ProtoElemJson` impls for it, which the orphan rule
816    ///   forbids for a foreign type. Wrap it in a crate-local newtype for those
817    ///   cases; singular / optional / oneof uses work directly.
818    /// - A `Custom` rule **does** apply to `map<K, bytes>` values (honored like
819    ///   the built-in [`BytesRepr::Bytes`]); only the `map<bytes, bytes>`
820    ///   carve-out keeps `Vec<u8>` values.
821    /// - A `path` that does not parse as a Rust type is reported as a codegen
822    ///   error from [`compile`](Self::compile).
823    /// - A custom bytes type needs no native `arbitrary::Arbitrary` impl (a
824    ///   generic builder handles it under `generate_arbitrary`).
825    #[must_use]
826    pub fn bytes_type_custom_in(self, path: &str, paths: &[impl AsRef<str>]) -> Self {
827        self.bytes_type_in(BytesRepr::Custom(path.to_string()), paths)
828    }
829
830    /// Map every `bytes` field to the given custom type path. Convenience for
831    /// `.bytes_type_custom_in(path, &["."])`; see it for the limitations
832    /// (foreign `repeated` elements, `map` values, path parsing).
833    #[must_use]
834    pub fn bytes_type_custom(self, path: &str) -> Self {
835        self.bytes_type(BytesRepr::Custom(path.to_string()))
836    }
837
838    /// Store the matching message-typed oneof variants inline instead of
839    /// wrapping them in `Box<T>`.
840    ///
841    /// By default every message/group oneof variant is boxed so that recursive
842    /// types compile. For non-recursive variants the `Box` is pure overhead (an
843    /// allocation per construction); this opts the matching variants out.
844    /// This affects the owned message enum only — view oneof variants remain
845    /// boxed.
846    ///
847    /// Each path is a fully-qualified proto variant path prefix, e.g.
848    /// `".my.pkg.MyMessage.body.small"` for one variant or `".my.pkg"` for a
849    /// package (same matching as [`use_bytes_type_in`](Self::use_bytes_type_in)).
850    /// A leading dot is added if missing, mirroring
851    /// [`extern_path`](Self::extern_path).
852    ///
853    /// Recursive variants cannot be stored inline (the type would be
854    /// unsized). A rule that names a recursive variant *exactly* is rejected
855    /// at codegen time; a broader prefix rule silently keeps recursive
856    /// variants boxed and inlines the rest. For example, with
857    /// `unbox_oneof_in(&[".my.pkg.Node"])`, a self-referential
858    /// `Node.kind.child` variant stays boxed while `Node`'s other message
859    /// variants become inline.
860    #[must_use]
861    pub fn unbox_oneof_in(mut self, paths: &[impl AsRef<str>]) -> Self {
862        self.codegen_config
863            .unboxed_oneof_fields
864            .extend(paths.iter().map(|p| {
865                let p = p.as_ref();
866                // Normalize to the leading-dot form: matching and the
867                // exact-path recursion error both depend on it.
868                if p.starts_with('.') {
869                    p.to_string()
870                } else {
871                    format!(".{p}")
872                }
873            }));
874        self
875    }
876
877    /// Store every non-recursive message-typed oneof variant inline instead of
878    /// boxing it. Convenience for `.unbox_oneof_in(&["."])`; recursive
879    /// variants stay boxed.
880    #[must_use]
881    pub fn unbox_oneof(mut self) -> Self {
882        self.codegen_config
883            .unboxed_oneof_fields
884            .push(".".to_string());
885        self
886    }
887
888    /// Map `string` fields to a [`StringRepr`] other than `String` for the
889    /// given proto path prefixes. The string counterpart to
890    /// [`use_bytes_type_in`](Self::use_bytes_type_in).
891    ///
892    /// Each path is a fully-qualified proto path prefix (e.g.
893    /// `".my.pkg.MyMessage.name"` for one field, `".my.pkg"` for a package).
894    ///
895    /// Rules accumulate and the **last** matching rule wins. Order therefore
896    /// matters: call [`string_type`](Self::string_type) (the broad default)
897    /// *first*, then `string_type_in` for narrower overrides — a broad rule
898    /// added after a specific one will shadow it.
899    ///
900    /// For [`StringRepr::Custom`], the type must implement `buffa::ProtoString`,
901    /// and the downstream crate must depend on the crate providing it (buffa does
902    /// not re-export it). A foreign type cannot implement `ProtoString` directly
903    /// (orphan rule) — point at a local newtype, or the `buffa-smolstr` crate for
904    /// `smol_str::SmolStr`.
905    ///
906    /// Only the owned Rust type changes: the wire format is unchanged, view
907    /// types still borrow `&str`, and `map<_, string>` keys and values stay
908    /// `String`.
909    ///
910    /// # Example
911    ///
912    /// ```rust,ignore
913    /// buffa_build::Config::new()
914    ///     .string_type_custom("::buffa_smolstr::SmolStr")  // broad default first
915    ///     .string_type_custom_in("::my_crate::CompactStr", &[".my.pkg.Msg.body"]) // narrow override
916    ///     .files(&["proto/my_service.proto"])
917    ///     .includes(&["proto/"])
918    ///     .compile()
919    ///     .unwrap();
920    /// ```
921    #[must_use]
922    pub fn string_type_in(mut self, repr: StringRepr, paths: &[impl AsRef<str>]) -> Self {
923        self.codegen_config
924            .string_fields
925            .extend(paths.iter().map(|p| (p.as_ref().to_string(), repr.clone())));
926        self
927    }
928
929    /// Map every `string` field in all messages to the given [`StringRepr`].
930    ///
931    /// Convenience for `.string_type_in(repr, &["."])`. Call this *before* any
932    /// [`string_type_in`](Self::string_type_in) overrides, since the last
933    /// matching rule wins (a `"."` rule added later shadows earlier specific
934    /// rules). `map<_, string>` keys and values stay `String`.
935    #[must_use]
936    pub fn string_type(mut self, repr: StringRepr) -> Self {
937        self.codegen_config
938            .string_fields
939            .push((".".to_string(), repr));
940        self
941    }
942
943    /// Map the matching `string` fields to a custom type that implements
944    /// `buffa::ProtoString`, named by its fully-qualified Rust path (e.g.
945    /// `"::buffa_smolstr::SmolStr"`, or a local newtype — a foreign type cannot
946    /// implement the trait directly). The downstream crate must depend on the
947    /// crate providing it. Shorthand for
948    /// [`string_type_in`](Self::string_type_in)`(StringRepr::Custom(path), paths)`.
949    ///
950    /// # Limitations
951    ///
952    /// - A **foreign** custom type used as a `repeated` element fails to compile:
953    ///   codegen emits a `ReflectElement` impl for it, which the orphan rule
954    ///   forbids for a foreign type. Wrap it in a crate-local newtype for the
955    ///   repeated case; singular / optional / oneof uses work directly.
956    /// - **JSON of a `repeated` custom string** serializes elements through their
957    ///   native `serde`, so such a type must derive `Serialize` / `Deserialize`
958    ///   (and an external type must enable its `serde` feature). Singular /
959    ///   optional / oneof custom strings use the `proto_string` with-module and
960    ///   need no `serde` impl.
961    /// - A `path` that does not parse as a Rust type is reported as a codegen
962    ///   error from [`compile`](Self::compile).
963    /// - A custom string type needs no native `arbitrary::Arbitrary` impl (a
964    ///   generic builder handles it under `generate_arbitrary`).
965    #[must_use]
966    pub fn string_type_custom_in(self, path: &str, paths: &[impl AsRef<str>]) -> Self {
967        self.string_type_in(StringRepr::Custom(path.to_string()), paths)
968    }
969
970    /// Map every `string` field to the given custom type path. Convenience for
971    /// `.string_type_custom_in(path, &["."])`; see it for the limitations
972    /// (foreign `repeated` elements, the `repeated` JSON `serde` requirement,
973    /// path parsing).
974    #[must_use]
975    pub fn string_type_custom(self, path: &str) -> Self {
976        self.string_type(StringRepr::Custom(path.to_string()))
977    }
978
979    /// Map the matching `map` fields to a [`MapRepr`] other than the default
980    /// `HashMap`. Rules are matched with proto-segment-aware prefix logic; the
981    /// **last** matching rule wins, so add a broad rule first and narrower
982    /// overrides after.
983    ///
984    /// Use [`MapRepr::BTreeMap`] for the buffa-provided `BTreeMap` (deterministic
985    /// key order, no extra dependency, no consumer code), or
986    /// [`MapRepr::Custom`] for a crate-local newtype that implements
987    /// `buffa::map_codec::MapStorage`.
988    ///
989    /// Only the owned collection changes: the wire format is unchanged and view
990    /// types are unaffected.
991    ///
992    /// # Example
993    ///
994    /// ```rust,ignore
995    /// buffa_build::Config::new()
996    ///     .map_type(buffa_build::MapRepr::BTreeMap)                       // broad default
997    ///     .map_type_in(buffa_build::MapRepr::HashMap, &[".my.pkg.Msg.cache"]) // narrow override
998    ///     .compile()
999    ///     .unwrap();
1000    /// ```
1001    #[must_use]
1002    pub fn map_type_in(mut self, repr: MapRepr, paths: &[impl AsRef<str>]) -> Self {
1003        self.codegen_config
1004            .map_fields
1005            .extend(paths.iter().map(|p| (p.as_ref().to_string(), repr.clone())));
1006        self
1007    }
1008
1009    /// Map every `map` field in all messages to the given [`MapRepr`].
1010    /// Convenience for `.map_type_in(repr, &["."])`. Call this *before* any
1011    /// [`map_type_in`](Self::map_type_in) overrides, since the last matching
1012    /// rule wins.
1013    #[must_use]
1014    pub fn map_type(mut self, repr: MapRepr) -> Self {
1015        self.codegen_config.map_fields.push((".".to_string(), repr));
1016        self
1017    }
1018
1019    /// Map the matching `map` fields to a custom collection implementing
1020    /// `buffa::map_codec::MapStorage`, named by its fully-qualified Rust path
1021    /// (e.g. `"::my_crate::OrderedMap"`). The path must **not** include the
1022    /// `<K, V>` parameters — they are applied positionally. Shorthand for
1023    /// [`map_type_in`](Self::map_type_in)`(MapRepr::Custom(path), paths)`.
1024    ///
1025    /// # Limitations
1026    ///
1027    /// - The path must name a **crate-local newtype** — a foreign map cannot
1028    ///   implement the buffa-owned reflection / serde traits (orphan rule).
1029    ///   Prefer the built-in [`MapRepr::BTreeMap`] unless you need a specific
1030    ///   foreign map.
1031    /// - The newtype must implement `buffa::MapStorage` plus the derive /
1032    ///   `FromIterator` / `ReflectMap` / serde / `arbitrary` bounds documented on
1033    ///   `buffa::map_codec::MapStorage` (the canonical list). JSON and
1034    ///   `arbitrary` work for every proto map key/value type regardless of the
1035    ///   container.
1036    /// - A path that does not parse as a Rust type is reported as a codegen
1037    ///   error from [`compile`](Self::compile).
1038    #[must_use]
1039    pub fn map_type_custom_in(self, path: &str, paths: &[impl AsRef<str>]) -> Self {
1040        self.map_type_in(MapRepr::Custom(path.to_string()), paths)
1041    }
1042
1043    /// Map every `map` field to the given custom collection path. Convenience
1044    /// for `.map_type_custom_in(path, &["."])`; see it for the limitations (the
1045    /// crate-local newtype requirement, the trait bounds, path parsing).
1046    #[must_use]
1047    pub fn map_type_custom(self, path: &str) -> Self {
1048        self.map_type(MapRepr::Custom(path.to_string()))
1049    }
1050
1051    /// Map the matching message fields to a [`PointerRepr`] other than the
1052    /// default `Box`. Rules are matched with proto-segment-aware prefix logic;
1053    /// the **last** matching rule wins, so add a broad rule first and narrower
1054    /// overrides after.
1055    ///
1056    /// Applies to singular (and proto2 optional/required) message fields and to
1057    /// **boxed** oneof message/group variants (matched by the variant's path).
1058    /// A oneof variant opted into inline storage via [`unbox_oneof_in`](Self::unbox_oneof_in)
1059    /// takes precedence and gets no pointer; recursive variants stay boxed and so
1060    /// accept a custom pointer. Repeated message fields use a collection, not a
1061    /// pointer. For [`PointerRepr::Custom`], the pointer must implement
1062    /// `buffa::ProtoBox<T>` and be a crate-local newtype; the path is a
1063    /// **template** with a `*` placeholder for the message type (e.g.
1064    /// `"::my_crate::SmallBox<*>"`).
1065    ///
1066    /// Only the in-memory pointer changes: the wire format is unchanged and view
1067    /// types are unaffected.
1068    #[must_use]
1069    pub fn box_type_in(mut self, repr: PointerRepr, paths: &[impl AsRef<str>]) -> Self {
1070        self.codegen_config
1071            .pointer_fields
1072            .extend(paths.iter().map(|p| (p.as_ref().to_string(), repr.clone())));
1073        self
1074    }
1075
1076    /// Map every message field (and boxed oneof variant) to the given [`PointerRepr`].
1077    /// Convenience for `.box_type_in(repr, &["."])`. Call before any
1078    /// [`box_type_in`](Self::box_type_in) overrides, since the last matching rule
1079    /// wins. An inline pointer inflates each parent struct, so prefer narrow
1080    /// rules over a blanket default.
1081    #[must_use]
1082    pub fn box_type(mut self, repr: PointerRepr) -> Self {
1083        self.codegen_config
1084            .pointer_fields
1085            .push((".".to_string(), repr));
1086        self
1087    }
1088
1089    /// Map the matching singular message fields to a custom pointer implementing
1090    /// `buffa::ProtoBox<T>`, named by a Rust type-path **template** with a `*`
1091    /// placeholder for the message type (e.g. `"::my_crate::SmallBox<*>"`).
1092    /// Shorthand for
1093    /// [`box_type_in`](Self::box_type_in)`(PointerRepr::Custom(template), paths)`.
1094    ///
1095    /// # Limitations
1096    ///
1097    /// - The template must contain at least one `*`; a template that omits it,
1098    ///   or whose substitution does not parse as a Rust type, is reported as a
1099    ///   codegen error from [`compile`](Self::compile).
1100    /// - The pointer must be exclusively owned (`Rc`/`Arc` are unusable — the
1101    ///   decoder needs `DerefMut`) and a crate-local newtype (a foreign pointer
1102    ///   cannot implement the buffa-owned `ProtoBox`).
1103    #[must_use]
1104    pub fn box_type_custom_in(self, template: &str, paths: &[impl AsRef<str>]) -> Self {
1105        self.box_type_in(PointerRepr::Custom(template.to_string()), paths)
1106    }
1107
1108    /// Map every message field (and boxed oneof variant) to the given custom pointer template.
1109    /// Convenience for `.box_type_custom_in(template, &["."])`; see it for the
1110    /// limitations (the `*` placeholder, `Rc`/`Arc` exclusion, newtype rule).
1111    #[must_use]
1112    pub fn box_type_custom(self, template: &str) -> Self {
1113        self.box_type(PointerRepr::Custom(template.to_string()))
1114    }
1115
1116    /// Map the matching `repeated` fields to a [`RepeatedRepr`] other than the
1117    /// default `Vec<T>`. Rules are matched with proto-segment-aware prefix
1118    /// logic; the **last** matching rule wins, so add a broad rule first and
1119    /// narrower overrides after. Applies only to `repeated` fields (not `map`).
1120    ///
1121    /// For [`RepeatedRepr::Custom`], the collection must implement
1122    /// `buffa::ProtoList<T>`. Unlike the scalar `string_type_custom` /
1123    /// `bytes_type_custom` knobs (which take a *complete* type path), this path
1124    /// is a **template** with a `*` placeholder for the element type, and it must
1125    /// name a **crate-local newtype** (a foreign collection cannot implement the
1126    /// buffa-owned `ProtoList`).
1127    ///
1128    /// Only the owned collection changes: the wire format is unchanged and view
1129    /// types still borrow `&[T]`.
1130    ///
1131    /// # Example
1132    ///
1133    /// ```rust,ignore
1134    /// // `SmallList<T>` is a crate-local newtype over smallvec::SmallVec that
1135    /// // implements buffa::ProtoList (see the ProtoList docs for the template).
1136    /// buffa_build::Config::new()
1137    ///     .repeated_type_custom("::my_crate::SmallList<*>")               // broad default
1138    ///     .repeated_type_custom_in("::my_crate::SmallList8<*>", &[".my.pkg.Msg.tags"])
1139    ///     .compile()
1140    ///     .unwrap();
1141    /// ```
1142    #[must_use]
1143    pub fn repeated_type_in(mut self, repr: RepeatedRepr, paths: &[impl AsRef<str>]) -> Self {
1144        self.codegen_config
1145            .repeated_fields
1146            .extend(paths.iter().map(|p| (p.as_ref().to_string(), repr.clone())));
1147        self
1148    }
1149
1150    /// Map every `repeated` field in all messages to the given
1151    /// [`RepeatedRepr`]. Convenience for `.repeated_type_in(repr, &["."])`.
1152    /// Call this *before* any [`repeated_type_in`](Self::repeated_type_in)
1153    /// overrides, since the last matching rule wins.
1154    #[must_use]
1155    pub fn repeated_type(mut self, repr: RepeatedRepr) -> Self {
1156        self.codegen_config
1157            .repeated_fields
1158            .push((".".to_string(), repr));
1159        self
1160    }
1161
1162    /// Map the matching `repeated` fields to a custom collection implementing
1163    /// `buffa::ProtoList<T>`, named by a Rust type-path **template** with a `*`
1164    /// placeholder for the element type (e.g. `"::my_crate::SmallList<*>"`).
1165    /// Note the asymmetry with the scalar `string_type_custom` /
1166    /// `bytes_type_custom` knobs: those take a *complete* path, this takes a
1167    /// `*`-template that wraps the element. Shorthand for
1168    /// [`repeated_type_in`](Self::repeated_type_in)`(RepeatedRepr::Custom(template), paths)`.
1169    ///
1170    /// # Limitations
1171    ///
1172    /// - The template must contain at least one `*`; a template that omits it,
1173    ///   or whose substitution does not parse as a Rust type, is reported as a
1174    ///   codegen error from [`compile`](Self::compile).
1175    /// - The template must name a **crate-local newtype** — a foreign collection
1176    ///   cannot implement the buffa-owned `ProtoList` (orphan rule). This applies
1177    ///   to *every* build, not just reflection: the generated decode and clear
1178    ///   code require `Field: ProtoList`.
1179    /// - Under reflection / vtable the newtype must also implement
1180    ///   `buffa_descriptor`'s `ReflectList` (not derivable, but a `Vec`-backed
1181    ///   newtype can delegate to the inner `Vec<T>`). Under JSON it must
1182    ///   implement `serde::Serialize` / `Deserialize`; under `generate_arbitrary`,
1183    ///   `arbitrary::Arbitrary` (derivable on a newtype). See `buffa::ProtoList`
1184    ///   for a worked newtype example.
1185    #[must_use]
1186    pub fn repeated_type_custom_in(self, template: &str, paths: &[impl AsRef<str>]) -> Self {
1187        self.repeated_type_in(RepeatedRepr::Custom(template.to_string()), paths)
1188    }
1189
1190    /// Map every `repeated` field to the given custom collection template.
1191    /// Convenience for `.repeated_type_custom_in(template, &["."])`; see it for
1192    /// the limitations (the `*` placeholder, foreign reflection, the JSON /
1193    /// `arbitrary` requirements).
1194    #[must_use]
1195    pub fn repeated_type_custom(self, template: &str) -> Self {
1196        self.repeated_type(RepeatedRepr::Custom(template.to_string()))
1197    }
1198
1199    /// Add a custom attribute to generated types (messages and enums)
1200    /// matching a proto path prefix.
1201    ///
1202    /// `path` is a fully-qualified proto path prefix: `"."` applies to all
1203    /// types, `".my.pkg"` to types in that package, `".my.pkg.MyMessage"`
1204    /// to a specific type. A leading `.` is auto-prepended if omitted; a
1205    /// trailing `.` is trimmed. Prefix matching respects proto-segment
1206    /// boundaries, so `".my.pk"` does not match `".my.pkg.Msg"`.
1207    ///
1208    /// `attribute` is a raw Rust attribute string
1209    /// (e.g., `"#[derive(serde::Serialize)]"`). A malformed attribute
1210    /// produces [`CodeGenError::InvalidCustomAttribute`](buffa_codegen::CodeGenError)
1211    /// at compile time rather than being silently dropped.
1212    ///
1213    /// Multiple calls accumulate in insertion order — all matching attributes
1214    /// are emitted, and ordering is preserved in generated code.
1215    ///
1216    /// Also applies to generated oneof enums when `path` matches
1217    /// `".pkg.Msg.my_oneof"` (the oneof's fully-qualified path).
1218    ///
1219    /// # Pitfalls
1220    ///
1221    /// buffa already emits `#[derive(Clone, PartialEq)]` on messages and
1222    /// `#[derive(Clone, PartialEq, Debug)]` on oneofs (oneofs with a
1223    /// `[debug_redact = true]` variant get a generated `Debug` impl instead
1224    /// of the `Debug` derive); adding a duplicate derive via
1225    /// `type_attribute(".", "#[derive(Clone)]")` produces a compile error in
1226    /// the generated code.
1227    ///
1228    /// # Example
1229    ///
1230    /// ```rust,ignore
1231    /// buffa_build::Config::new()
1232    ///     .type_attribute(".", "#[derive(serde::Serialize)]")
1233    ///     .type_attribute(".my.pkg.MyEnum", "#[derive(strum::EnumIter)]")
1234    ///     .files(&["proto/my_service.proto"])
1235    ///     .includes(&["proto/"])
1236    ///     .compile()
1237    ///     .unwrap();
1238    /// ```
1239    #[must_use]
1240    pub fn type_attribute(mut self, path: impl Into<String>, attribute: impl Into<String>) -> Self {
1241        self.codegen_config
1242            .type_attributes
1243            .push((normalize_attr_path(path.into()), attribute.into()));
1244        self
1245    }
1246
1247    /// Add a custom attribute to generated struct fields matching a proto
1248    /// path prefix.
1249    ///
1250    /// `path` is a fully-qualified proto field path (e.g.,
1251    /// `".my.pkg.MyMessage.my_field"`). `"."` applies to all fields. A
1252    /// leading `.` is auto-prepended if omitted; a trailing `.` is trimmed.
1253    /// Prefix matching respects proto-segment boundaries.
1254    ///
1255    /// Also applies to oneof variants when `path` matches
1256    /// `".pkg.Msg.my_oneof.variant_name"`.
1257    ///
1258    /// # Example
1259    ///
1260    /// ```rust,ignore
1261    /// buffa_build::Config::new()
1262    ///     .field_attribute(".my.pkg.MyMessage.secret_key", "#[serde(skip)]")
1263    ///     .files(&["proto/my_service.proto"])
1264    ///     .includes(&["proto/"])
1265    ///     .compile()
1266    ///     .unwrap();
1267    /// ```
1268    #[must_use]
1269    pub fn field_attribute(
1270        mut self,
1271        path: impl Into<String>,
1272        attribute: impl Into<String>,
1273    ) -> Self {
1274        self.codegen_config
1275            .field_attributes
1276            .push((normalize_attr_path(path.into()), attribute.into()));
1277        self
1278    }
1279
1280    /// Add a custom attribute to generated message structs only (not enums,
1281    /// not oneof enums — those are reached by
1282    /// [`enum_attribute`](Self::enum_attribute) and
1283    /// [`oneof_attribute`](Self::oneof_attribute) respectively) matching a
1284    /// proto path prefix.
1285    ///
1286    /// Same path-matching semantics as [`type_attribute`](Self::type_attribute) —
1287    /// leading `.` auto-prepended, trailing `.` trimmed, proto-segment-aware
1288    /// prefix matching, accumulation in insertion order. A malformed attribute
1289    /// produces a compile-time error. Useful for struct-only attributes like
1290    /// `#[serde(default)]`.
1291    ///
1292    /// # Example
1293    ///
1294    /// ```rust,ignore
1295    /// buffa_build::Config::new()
1296    ///     .message_attribute(".", "#[serde(default)]")
1297    ///     .files(&["proto/my_service.proto"])
1298    ///     .includes(&["proto/"])
1299    ///     .compile()
1300    ///     .unwrap();
1301    /// ```
1302    #[must_use]
1303    pub fn message_attribute(
1304        mut self,
1305        path: impl Into<String>,
1306        attribute: impl Into<String>,
1307    ) -> Self {
1308        self.codegen_config
1309            .message_attributes
1310            .push((normalize_attr_path(path.into()), attribute.into()));
1311        self
1312    }
1313
1314    /// Add a custom attribute to generated enum types only (not message
1315    /// structs, not oneof enums — those are reached by
1316    /// [`type_attribute`](Self::type_attribute) on the oneof's path or by
1317    /// [`oneof_attribute`](Self::oneof_attribute)) matching a proto path
1318    /// prefix.
1319    ///
1320    /// Same path-matching semantics as [`type_attribute`](Self::type_attribute) —
1321    /// leading `.` auto-prepended, trailing `.` trimmed, proto-segment-aware
1322    /// prefix matching, accumulation in insertion order. A malformed attribute
1323    /// produces a compile-time error. Useful when you want to inject an
1324    /// attribute on every enum in a package without also matching the
1325    /// (often more numerous) messages that share the path prefix — e.g.
1326    /// `#[derive(strum::EnumIter)]`, which only makes sense on enums.
1327    ///
1328    /// # Example
1329    ///
1330    /// ```rust,ignore
1331    /// buffa_build::Config::new()
1332    ///     .enum_attribute(".my.pkg", "#[derive(strum::EnumIter)]")
1333    ///     .files(&["proto/my_service.proto"])
1334    ///     .includes(&["proto/"])
1335    ///     .compile()
1336    ///     .unwrap();
1337    /// ```
1338    #[must_use]
1339    pub fn enum_attribute(mut self, path: impl Into<String>, attribute: impl Into<String>) -> Self {
1340        self.codegen_config
1341            .enum_attributes
1342            .push((normalize_attr_path(path.into()), attribute.into()));
1343        self
1344    }
1345
1346    /// Add a custom attribute to generated oneof enums only (not message
1347    /// structs, not regular enums) matching a proto path prefix.
1348    ///
1349    /// Same path-matching semantics as [`type_attribute`](Self::type_attribute):
1350    /// a leading `.` is auto-prepended, a trailing `.` is trimmed, prefixes
1351    /// match on proto-path segments, and attributes accumulate in insertion
1352    /// order. The match key is the oneof's fully-qualified path
1353    /// (`.my.pkg.MyMessage.my_oneof`) — the whole-enum path has no variant
1354    /// segment; to target a single variant's field, append `.variant_name`
1355    /// and use [`field_attribute`](Self::field_attribute) instead. A
1356    /// malformed attribute produces a compile-time error in the generated
1357    /// code. Useful when a oneof needs a different attribute set than the
1358    /// surrounding types — for example to keep `#[derive(serde::Serialize)]`
1359    /// on messages and oneofs while
1360    /// [`enum_attribute`](Self::enum_attribute) gives the regular enums a
1361    /// different serde derive.
1362    ///
1363    /// Applies to the owned oneof enum only; the zero-copy view-of-oneof
1364    /// enum receives no custom attributes (true of the whole attribute
1365    /// family). For JSON serialization of both owned types and views, use
1366    /// [`generate_json(true)`](Self::generate_json), which emits canonical
1367    /// protobuf-JSON impls rather than derived ones.
1368    ///
1369    /// # Pitfalls
1370    ///
1371    /// Generated oneof enums already derive `Clone`, `PartialEq`, and
1372    /// `Debug` (oneofs containing `[debug_redact = true]` fields replace the
1373    /// `Debug` derive with a manual impl). Re-deriving any of these via
1374    /// `oneof_attribute` produces a conflicting-implementation compile error
1375    /// inside the generated code.
1376    ///
1377    /// # Example
1378    ///
1379    /// ```rust,ignore
1380    /// buffa_build::Config::new()
1381    ///     // one specific oneof; ".my.pkg" would match every oneof in the package
1382    ///     .oneof_attribute(".my.pkg.MyMessage.my_oneof", "#[derive(serde::Serialize)]")
1383    ///     .files(&["proto/my_service.proto"])
1384    ///     .includes(&["proto/"])
1385    ///     .compile()
1386    ///     .unwrap();
1387    /// ```
1388    #[must_use]
1389    pub fn oneof_attribute(
1390        mut self,
1391        path: impl Into<String>,
1392        attribute: impl Into<String>,
1393    ) -> Self {
1394        self.codegen_config
1395            .oneof_attributes
1396            .push((normalize_attr_path(path.into()), attribute.into()));
1397        self
1398    }
1399
1400    /// Use `buf build` instead of `protoc` for descriptor generation.
1401    ///
1402    /// `buf` is often easier to install and keep current than `protoc`
1403    /// (which many distros pin to old versions). This mode is intended for
1404    /// the **single-crate case**: a `buf.yaml` at the crate root defining
1405    /// the module layout.
1406    ///
1407    /// Requires `buf` on PATH and a `buf.yaml` at the crate root. The
1408    /// [`includes()`](Self::includes) setting is ignored — buf resolves
1409    /// imports via its own module configuration.
1410    ///
1411    /// Each path given to [`files()`](Self::files) must be **relative to its
1412    /// owning module's directory** (the `path:` value inside `buf.yaml`), not
1413    /// the crate root where `buf.yaml` itself lives. buf strips the module
1414    /// path when producing `FileDescriptorProto.name`, so for
1415    /// `modules: [{path: proto}]` and a file on disk at
1416    /// `proto/api/v1/service.proto`, the descriptor name is
1417    /// `api/v1/service.proto` — that is what `.files()` must contain.
1418    /// Multiple modules in one `buf.yaml` work fine; buf enforces that
1419    /// module-relative names are unique across the workspace.
1420    ///
1421    /// # Monorepo / multi-module setups
1422    ///
1423    /// For a workspace-root `buf.yaml` with many modules, this mode is a
1424    /// poor fit. Prefer running `buf generate` with the `protoc-gen-buffa`
1425    /// plugin and checking in the generated code, or use
1426    /// [`descriptor_set()`](Self::descriptor_set) with the output of
1427    /// `buf build --as-file-descriptor-set -o fds.binpb <module-path>`
1428    /// run as a pre-build step.
1429    ///
1430    /// # Example
1431    ///
1432    /// ```rust,ignore
1433    /// // buf.yaml (at crate root):
1434    /// //   version: v2
1435    /// //   modules:
1436    /// //     - path: proto
1437    /// //
1438    /// // build.rs:
1439    /// buffa_build::Config::new()
1440    ///     .use_buf()
1441    ///     .files(&["api/v1/service.proto"])  // relative to module root
1442    ///     .compile()
1443    ///     .unwrap();
1444    /// ```
1445    #[must_use]
1446    pub fn use_buf(mut self) -> Self {
1447        self.descriptor_source = DescriptorSource::Buf;
1448        self
1449    }
1450
1451    /// Use a pre-compiled `FileDescriptorSet` binary file as input.
1452    ///
1453    /// Skips invoking `protoc` or `buf` entirely. The file must contain a
1454    /// serialized `google.protobuf.FileDescriptorSet` (as produced by
1455    /// `protoc --descriptor_set_out` or `buf build --as-file-descriptor-set`).
1456    ///
1457    /// When using this, `.files()` specifies which proto files in the
1458    /// descriptor set to generate code for (matching by proto file name).
1459    #[must_use]
1460    pub fn descriptor_set(mut self, path: impl Into<PathBuf>) -> Self {
1461        self.descriptor_source = DescriptorSource::Precompiled(path.into());
1462        self
1463    }
1464
1465    /// Generate a module-tree include file alongside the per-package `.rs`
1466    /// files.
1467    ///
1468    /// The include file contains nested `pub mod` declarations with
1469    /// `include!()` directives that assemble the generated code into a
1470    /// module hierarchy matching the protobuf package structure. Users can
1471    /// then include this single file instead of manually creating the
1472    /// module tree.
1473    ///
1474    /// The form of the emitted `include!` directives depends on whether
1475    /// [`out_dir`](Self::out_dir) was set:
1476    ///
1477    /// - **Default (`$OUT_DIR`)**: emits
1478    ///   `include!(concat!(env!("OUT_DIR"), "/foo.rs"))`, for use from
1479    ///   `build.rs` via `include!(concat!(env!("OUT_DIR"), "/<name>"))`.
1480    /// - **Explicit `out_dir`**: emits sibling-relative `include!("foo.rs")`,
1481    ///   for checking the generated code into the source tree and referencing
1482    ///   it as a module (e.g. `mod gen;`).
1483    ///
1484    /// # Example — `build.rs` / `$OUT_DIR`
1485    ///
1486    /// ```rust,ignore
1487    /// // build.rs
1488    /// buffa_build::Config::new()
1489    ///     .files(&["proto/my_service.proto"])
1490    ///     .includes(&["proto/"])
1491    ///     .include_file("_include.rs")
1492    ///     .compile()
1493    ///     .unwrap();
1494    ///
1495    /// // lib.rs
1496    /// include!(concat!(env!("OUT_DIR"), "/_include.rs"));
1497    /// ```
1498    ///
1499    /// # Example — checked-in source
1500    ///
1501    /// ```rust,ignore
1502    /// // codegen.rs (run manually, not from build.rs)
1503    /// buffa_build::Config::new()
1504    ///     .files(&["proto/my_service.proto"])
1505    ///     .includes(&["proto/"])
1506    ///     .out_dir("src/gen")
1507    ///     .include_file("mod.rs")
1508    ///     .compile()
1509    ///     .unwrap();
1510    ///
1511    /// // lib.rs
1512    /// mod gen;
1513    /// ```
1514    #[must_use]
1515    pub fn include_file(mut self, name: impl Into<String>) -> Self {
1516        self.include_file = Some(name.into());
1517        self
1518    }
1519
1520    /// Compile proto files and generate Rust source.
1521    ///
1522    /// # Errors
1523    ///
1524    /// Returns an error if:
1525    /// - `OUT_DIR` is not set and no `out_dir` was configured
1526    /// - `protoc` or `buf` cannot be found on `PATH` (when using those sources)
1527    /// - the proto compiler exits with a non-zero status (syntax errors,
1528    ///   missing imports, etc.)
1529    /// - a precompiled descriptor set file cannot be read
1530    /// - the descriptor set bytes cannot be decoded as a `FileDescriptorSet`
1531    /// - code generation fails (e.g. unsupported proto feature)
1532    /// - the output directory cannot be created or written to
1533    pub fn compile(self) -> Result<(), Box<dyn std::error::Error>> {
1534        // When out_dir is explicitly set, the include file should use
1535        // relative `include!("foo.rs")` paths (the index is a sibling of the
1536        // generated files). When defaulted to $OUT_DIR, keep the
1537        // `concat!(env!("OUT_DIR"), ...)` form so that
1538        // `include!(concat!(env!("OUT_DIR"), "/_include.rs"))` from src/
1539        // still resolves to absolute paths.
1540        let relative_includes = self.out_dir.is_some();
1541        let out_dir = self
1542            .out_dir
1543            .or_else(|| std::env::var("OUT_DIR").ok().map(PathBuf::from))
1544            .ok_or("OUT_DIR not set and no out_dir configured")?;
1545
1546        // Produce a FileDescriptorSet from the configured source.
1547        let descriptor_bytes = match &self.descriptor_source {
1548            DescriptorSource::Protoc => invoke_protoc(&self.files, &self.includes)?,
1549            DescriptorSource::Buf => invoke_buf()?,
1550            DescriptorSource::Precompiled(path) => std::fs::read(path).map_err(|e| {
1551                format!("failed to read descriptor set '{}': {}", path.display(), e)
1552            })?,
1553        };
1554        let fds = FileDescriptorSet::decode_from_slice(&descriptor_bytes)
1555            .map_err(|e| format!("failed to decode FileDescriptorSet: {}", e))?;
1556
1557        // Determine which files were explicitly requested.
1558        //
1559        // `FileDescriptorProto.name` contains the path relative to the proto
1560        // source root (protoc: `--proto_path`; buf: the module root). For
1561        // Precompiled and Buf mode, `.files()` are expected to already be
1562        // proto-relative names. For Protoc mode, strip the longest matching
1563        // include prefix.
1564        let files_to_generate: Vec<String> = if matches!(
1565            self.descriptor_source,
1566            DescriptorSource::Precompiled(_) | DescriptorSource::Buf
1567        ) {
1568            self.files
1569                .iter()
1570                .filter_map(|f| f.to_str().map(str::to_string))
1571                .collect()
1572        } else {
1573            self.files
1574                .iter()
1575                .map(|f| proto_relative_name(f, &self.includes))
1576                .filter(|s| !s.is_empty())
1577                .collect()
1578        };
1579
1580        // Generate Rust source. Per-proto content files plus a per-package
1581        // `.mod.rs` stitcher; only the stitchers need wiring into the
1582        // module tree (content files are reached via `include!` from
1583        // there).
1584        let (generated, warnings) = buffa_codegen::generate_with_diagnostics(
1585            &fds.file,
1586            &files_to_generate,
1587            &self.codegen_config,
1588        )?;
1589
1590        // Surface non-fatal codegen diagnostics as Cargo build warnings. This
1591        // runs inside the consumer's `build.rs`, so `cargo:warning=` is shown in
1592        // their normal `cargo build` output.
1593        for warning in warnings {
1594            println!("cargo:warning=buffa: {warning}");
1595        }
1596
1597        // Write output files; collect (name, package) for PackageMod entries.
1598        let mut output_entries: Vec<(String, String)> = Vec::new();
1599        for file in generated {
1600            let path = out_dir.join(&file.name);
1601            if let Some(parent) = path.parent() {
1602                std::fs::create_dir_all(parent)?;
1603            }
1604            write_if_changed(&path, file.content.as_bytes())?;
1605            if file.kind == buffa_codegen::GeneratedFileKind::PackageMod {
1606                output_entries.push((file.name, file.package));
1607            }
1608        }
1609
1610        // Generate the include file if requested.
1611        if let Some(ref include_name) = self.include_file {
1612            let include_content = generate_include_file(&output_entries, relative_includes);
1613            let include_path = out_dir.join(include_name);
1614            write_if_changed(&include_path, include_content.as_bytes())?;
1615        }
1616
1617        // Tell cargo to re-run if any proto file changes.
1618        //
1619        // For Buf mode, `self.files` are module-root-relative and cargo can't
1620        // stat them — use `buf ls-files` instead, which lists all workspace
1621        // protos with workspace-relative paths. This also catches changes to
1622        // transitively-imported protos (a gap in the Protoc mode, which only
1623        // watches explicitly-listed files).
1624        match self.descriptor_source {
1625            DescriptorSource::Buf => emit_buf_rerun_if_changed(),
1626            DescriptorSource::Protoc => {
1627                // Rerun if PROTOC changes (different binary may accept
1628                // protos the previous one rejected, e.g. newer editions).
1629                println!("cargo:rerun-if-env-changed=PROTOC");
1630                for proto_file in &self.files {
1631                    println!("cargo:rerun-if-changed={}", proto_file.display());
1632                }
1633            }
1634            DescriptorSource::Precompiled(ref path) => {
1635                println!("cargo:rerun-if-changed={}", path.display());
1636            }
1637        }
1638
1639        Ok(())
1640    }
1641}
1642
1643impl Default for Config {
1644    fn default() -> Self {
1645        Self::new()
1646    }
1647}
1648
1649/// Normalize a user-supplied attribute-match path.
1650///
1651/// - Prepends `.` if absent so all stored paths are rooted.
1652/// - Trims trailing `.` so `".my.pkg."` and `".my.pkg"` behave identically
1653///   (trailing-dot patterns otherwise never match a real FQN).
1654/// - The bare catch-all `"."` is preserved as-is.
1655fn normalize_attr_path(mut path: String) -> String {
1656    if !path.starts_with('.') {
1657        path.insert(0, '.');
1658    }
1659    if path.len() > 1 {
1660        while path.ends_with('.') {
1661            path.pop();
1662        }
1663    }
1664    path
1665}
1666
1667/// Write `content` to `path` only if the file doesn't already exist with
1668/// identical content. Avoids bumping timestamps on unchanged files, which
1669/// prevents unnecessary downstream recompilation.
1670fn write_if_changed(path: &Path, content: &[u8]) -> std::io::Result<()> {
1671    if let Ok(existing) = std::fs::read(path) {
1672        if existing == content {
1673            return Ok(());
1674        }
1675    }
1676    std::fs::write(path, content)
1677}
1678
1679/// Invoke `protoc` to produce a `FileDescriptorSet` (serialized bytes).
1680fn invoke_protoc(
1681    files: &[PathBuf],
1682    includes: &[PathBuf],
1683) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
1684    let protoc = std::env::var("PROTOC").unwrap_or_else(|_| "protoc".to_string());
1685
1686    let descriptor_file =
1687        tempfile::NamedTempFile::new().map_err(|e| format!("failed to create temp file: {}", e))?;
1688    let descriptor_path = descriptor_file.path().to_path_buf();
1689
1690    let mut cmd = Command::new(&protoc);
1691    cmd.arg("--include_imports");
1692    cmd.arg("--include_source_info");
1693    cmd.arg(format!(
1694        "--descriptor_set_out={}",
1695        descriptor_path.display()
1696    ));
1697
1698    for include in includes {
1699        cmd.arg(format!("--proto_path={}", include.display()));
1700    }
1701
1702    for file in files {
1703        cmd.arg(file.as_os_str());
1704    }
1705
1706    let output = cmd
1707        .output()
1708        .map_err(|e| format!("failed to run protoc ({}): {}", protoc, e))?;
1709
1710    if !output.status.success() {
1711        let stderr = String::from_utf8_lossy(&output.stderr);
1712        return Err(format!("protoc failed: {}", stderr).into());
1713    }
1714
1715    let bytes = std::fs::read(&descriptor_path)
1716        .map_err(|e| format!("failed to read descriptor set: {}", e))?;
1717
1718    Ok(bytes)
1719}
1720
1721/// Invoke `buf build` to produce a `FileDescriptorSet` (serialized bytes).
1722///
1723/// Requires a `buf.yaml` discoverable from the build script's cwd. Builds
1724/// the entire workspace — no `--path` filtering, because buf's `--path` flag
1725/// expects workspace-relative paths while `FileDescriptorProto.name` is
1726/// module-root-relative; passing user paths to both would be a contradiction.
1727/// Codegen filtering happens on our side via `files_to_generate` matching.
1728fn invoke_buf() -> Result<Vec<u8>, Box<dyn std::error::Error>> {
1729    // buf build includes SourceCodeInfo by default (there's an
1730    // --exclude-source-info flag to disable it), so proto comments
1731    // propagate to generated code without an explicit opt-in here.
1732    let output = Command::new("buf")
1733        .arg("build")
1734        .arg("--as-file-descriptor-set")
1735        .arg("-o")
1736        .arg("-")
1737        .output()
1738        .map_err(|e| format!("failed to run buf (is it installed and on PATH?): {e}"))?;
1739
1740    if !output.status.success() {
1741        let stderr = String::from_utf8_lossy(&output.stderr);
1742        return Err(
1743            format!("buf build failed (is buf.yaml present at crate root?): {stderr}").into(),
1744        );
1745    }
1746
1747    Ok(output.stdout)
1748}
1749
1750/// Emit `cargo:rerun-if-changed` directives for a buf workspace.
1751///
1752/// Runs `buf ls-files` to discover all proto files with workspace-relative
1753/// paths (which cargo can stat). Also watches `buf.yaml` and `buf.lock`
1754/// (the latter only if it exists — cargo treats a missing rerun-if-changed
1755/// path as always-dirty). Failure is non-fatal: worst case cargo reruns
1756/// every build.
1757fn emit_buf_rerun_if_changed() {
1758    println!("cargo:rerun-if-changed=buf.yaml");
1759    if Path::new("buf.lock").exists() {
1760        println!("cargo:rerun-if-changed=buf.lock");
1761    }
1762    match Command::new("buf").arg("ls-files").output() {
1763        Ok(out) if out.status.success() => {
1764            for line in String::from_utf8_lossy(&out.stdout).lines() {
1765                let path = line.trim();
1766                if !path.is_empty() {
1767                    println!("cargo:rerun-if-changed={path}");
1768                }
1769            }
1770        }
1771        _ => {
1772            // ls-files failed; cargo already knows about buf.yaml above.
1773            // If buf itself is missing, invoke_buf() will error clearly.
1774        }
1775    }
1776}
1777
1778/// Convert a filesystem proto path to the name protoc uses in the descriptor.
1779///
1780/// `FileDescriptorProto.name` is relative to the `--proto_path` include
1781/// directory. This strips the longest matching include prefix; if no include
1782/// matches, returns the path as-is (not just file_name — that would break
1783/// nested proto directories).
1784fn proto_relative_name(file: &Path, includes: &[PathBuf]) -> String {
1785    // Longest prefix wins: a file under both "proto/" and "proto/vendor/"
1786    // should strip "proto/vendor/" for a correct relative name.
1787    let mut best: Option<&Path> = None;
1788    for include in includes {
1789        if let Ok(rel) = file.strip_prefix(include) {
1790            match best {
1791                Some(prev) if prev.as_os_str().len() <= rel.as_os_str().len() => {}
1792                _ => best = Some(rel),
1793            }
1794        }
1795    }
1796    best.unwrap_or(file).to_str().unwrap_or("").to_string()
1797}
1798
1799/// Generate the content of an include file that assembles generated `.rs`
1800/// files into a nested module tree matching the protobuf package hierarchy.
1801///
1802/// Each generated file is named like `my.package.file_name.rs`. The package
1803/// segments become `pub mod` wrappers, and the file is `include!`d inside
1804/// the innermost module.
1805///
1806/// For example, files `["foo.bar.rs", "foo.baz.rs"]` produce:
1807/// ```text
1808/// pub mod foo {
1809///     #[allow(unused_imports)]
1810///     use super::*;
1811///     include!(concat!(env!("OUT_DIR"), "/foo.bar.rs"));
1812///     include!(concat!(env!("OUT_DIR"), "/foo.baz.rs"));
1813/// }
1814/// ```
1815///
1816/// When `relative` is true (the caller set [`Config::out_dir`] explicitly),
1817/// `include!` directives use bare sibling paths (`include!("foo.bar.rs")`)
1818/// instead of the `env!("OUT_DIR")` prefix, so the include file works when
1819/// checked into the source tree and referenced via `mod`.
1820fn generate_include_file(entries: &[(String, String)], relative: bool) -> String {
1821    let mode = if relative {
1822        buffa_codegen::IncludeMode::Relative("")
1823    } else {
1824        buffa_codegen::IncludeMode::OutDir
1825    };
1826    // Inner-allow off: this output is consumed via `include!` from
1827    // user-authored `lib.rs`, where `#![allow(...)]` is not valid.
1828    buffa_codegen::generate_module_tree(entries, mode, false)
1829}
1830
1831#[cfg(test)]
1832mod tests {
1833    use super::*;
1834
1835    #[test]
1836    fn feature_name_setters_reach_codegen_config() {
1837        let config = Config::new()
1838            .json_feature_name("serde")
1839            .views_feature_name("zero-copy")
1840            .text_feature_name(String::from("textproto"))
1841            .reflect_feature_name("reflection")
1842            .codegen_config;
1843        let names = &config.feature_gate_names;
1844        assert_eq!(names.json, "serde");
1845        assert_eq!(names.views, "zero-copy");
1846        assert_eq!(names.text, "textproto");
1847        assert_eq!(names.reflect, "reflection");
1848    }
1849
1850    #[test]
1851    fn unbox_oneof_in_normalizes_leading_dot() {
1852        // Without normalization a dotless path would silently match nothing,
1853        // and the exact-path recursion error would never fire for it.
1854        let config = Config::new()
1855            .unbox_oneof_in(&["my.pkg.Msg.body.small", ".my.pkg.Other"])
1856            .codegen_config;
1857        assert_eq!(
1858            config.unboxed_oneof_fields,
1859            vec![
1860                ".my.pkg.Msg.body.small".to_string(),
1861                ".my.pkg.Other".to_string()
1862            ]
1863        );
1864    }
1865
1866    #[test]
1867    fn proto_relative_name_strips_include() {
1868        let got = proto_relative_name(
1869            Path::new("proto/my/service.proto"),
1870            &[PathBuf::from("proto/")],
1871        );
1872        assert_eq!(got, "my/service.proto");
1873    }
1874
1875    #[test]
1876    fn proto_relative_name_longest_prefix_wins() {
1877        // Overlapping includes: file under both proto/ and proto/vendor/.
1878        // Must strip the LONGER prefix for the correct relative name.
1879        let got = proto_relative_name(
1880            Path::new("proto/vendor/ext.proto"),
1881            &[PathBuf::from("proto/"), PathBuf::from("proto/vendor/")],
1882        );
1883        assert_eq!(got, "ext.proto");
1884        // Same with reversed include order.
1885        let got = proto_relative_name(
1886            Path::new("proto/vendor/ext.proto"),
1887            &[PathBuf::from("proto/vendor/"), PathBuf::from("proto/")],
1888        );
1889        assert_eq!(got, "ext.proto");
1890    }
1891
1892    #[test]
1893    fn proto_relative_name_no_match_returns_full_path() {
1894        // Regression: previously fell back to file_name(), which stripped
1895        // directory components and broke descriptor_set() mode with nested
1896        // proto packages. Now returns the full path as-is.
1897        let got = proto_relative_name(Path::new("my/pkg/service.proto"), &[]);
1898        assert_eq!(got, "my/pkg/service.proto");
1899    }
1900
1901    #[test]
1902    fn proto_relative_name_no_match_with_unrelated_includes() {
1903        let got = proto_relative_name(
1904            Path::new("src/my.proto"),
1905            &[PathBuf::from("other/"), PathBuf::from("third/")],
1906        );
1907        assert_eq!(got, "src/my.proto");
1908    }
1909
1910    #[test]
1911    fn include_file_out_dir_mode_uses_env_var() {
1912        let entries = vec![
1913            ("foo.bar.rs".to_string(), "foo".to_string()),
1914            ("root.rs".to_string(), String::new()),
1915        ];
1916        let out = generate_include_file(&entries, false);
1917        assert!(
1918            out.contains(r#"include!(concat!(env!("OUT_DIR"), "/foo.bar.rs"));"#),
1919            "nested-package file should use env!(OUT_DIR): {out}"
1920        );
1921        assert!(
1922            out.contains(r#"include!(concat!(env!("OUT_DIR"), "/root.rs"));"#),
1923            "empty-package file should use env!(OUT_DIR): {out}"
1924        );
1925        assert!(!out.contains(r#"include!("foo.bar.rs")"#));
1926    }
1927
1928    #[test]
1929    fn include_file_relative_mode_uses_sibling_paths() {
1930        let entries = vec![
1931            ("foo.bar.rs".to_string(), "foo".to_string()),
1932            ("root.rs".to_string(), String::new()),
1933        ];
1934        let out = generate_include_file(&entries, true);
1935        assert!(
1936            out.contains(r#"include!("foo.bar.rs");"#),
1937            "nested-package file should use relative path: {out}"
1938        );
1939        assert!(
1940            out.contains(r#"include!("root.rs");"#),
1941            "empty-package file should use relative path: {out}"
1942        );
1943        assert!(
1944            !out.contains("OUT_DIR"),
1945            "relative mode must not reference OUT_DIR: {out}"
1946        );
1947    }
1948
1949    #[test]
1950    fn include_file_relative_mode_nested_packages() {
1951        // Two files in the same depth-2 package: verifies the relative flag
1952        // propagates through recursive emit() calls and both files land in
1953        // the same innermost mod.
1954        let entries = vec![
1955            ("a.b.one.rs".to_string(), "a.b".to_string()),
1956            ("a.b.two.rs".to_string(), "a.b".to_string()),
1957        ];
1958        let out = generate_include_file(&entries, true);
1959        // Both includes should appear once, at the same depth-2 indent,
1960        // inside a single `pub mod b { ... }`.
1961        let indent = "        "; // depth 2 = 8 spaces
1962        assert!(
1963            out.contains(&format!(r#"{indent}include!("a.b.one.rs");"#)),
1964            "first file at depth 2: {out}"
1965        );
1966        assert!(
1967            out.contains(&format!(r#"{indent}include!("a.b.two.rs");"#)),
1968            "second file at depth 2: {out}"
1969        );
1970        assert_eq!(
1971            out.matches("pub mod b {").count(),
1972            1,
1973            "both files share one `mod b`: {out}"
1974        );
1975        assert!(!out.contains("OUT_DIR"));
1976    }
1977
1978    #[test]
1979    fn write_if_changed_creates_new_file() {
1980        let dir = tempfile::tempdir().unwrap();
1981        let path = dir.path().join("new.rs");
1982        write_if_changed(&path, b"hello").unwrap();
1983        assert_eq!(std::fs::read(&path).unwrap(), b"hello");
1984    }
1985
1986    #[test]
1987    fn write_if_changed_skips_identical_content() {
1988        let dir = tempfile::tempdir().unwrap();
1989        let path = dir.path().join("same.rs");
1990        std::fs::write(&path, b"content").unwrap();
1991        let mtime_before = std::fs::metadata(&path).unwrap().modified().unwrap();
1992
1993        // Sleep briefly so any write would produce a different mtime.
1994        std::thread::sleep(std::time::Duration::from_millis(50));
1995
1996        write_if_changed(&path, b"content").unwrap();
1997        let mtime_after = std::fs::metadata(&path).unwrap().modified().unwrap();
1998        assert_eq!(mtime_before, mtime_after);
1999    }
2000
2001    #[test]
2002    fn write_if_changed_overwrites_different_content() {
2003        let dir = tempfile::tempdir().unwrap();
2004        let path = dir.path().join("changed.rs");
2005        std::fs::write(&path, b"old").unwrap();
2006
2007        write_if_changed(&path, b"new").unwrap();
2008        assert_eq!(std::fs::read(&path).unwrap(), b"new");
2009    }
2010
2011    #[test]
2012    fn normalize_attr_path_prepends_leading_dot() {
2013        assert_eq!(normalize_attr_path("my.pkg".into()), ".my.pkg");
2014    }
2015
2016    #[test]
2017    fn normalize_attr_path_preserves_leading_dot() {
2018        assert_eq!(normalize_attr_path(".my.pkg".into()), ".my.pkg");
2019    }
2020
2021    #[test]
2022    fn normalize_attr_path_trims_trailing_dot() {
2023        assert_eq!(normalize_attr_path("my.pkg.".into()), ".my.pkg");
2024        assert_eq!(normalize_attr_path(".my.pkg.".into()), ".my.pkg");
2025        assert_eq!(normalize_attr_path(".my.pkg...".into()), ".my.pkg");
2026    }
2027
2028    #[test]
2029    fn normalize_attr_path_preserves_catchall() {
2030        assert_eq!(normalize_attr_path(".".into()), ".");
2031        assert_eq!(normalize_attr_path("".into()), ".");
2032    }
2033
2034    #[test]
2035    fn type_attribute_forwards_normalized_path() {
2036        let cfg = Config::new().type_attribute("my.pkg.", "#[derive(Foo)]");
2037        assert_eq!(
2038            cfg.codegen_config.type_attributes,
2039            vec![(".my.pkg".to_string(), "#[derive(Foo)]".to_string())]
2040        );
2041    }
2042
2043    #[test]
2044    fn field_attribute_forwards_normalized_path() {
2045        let cfg = Config::new().field_attribute("pkg.Msg.f", "#[serde(skip)]");
2046        assert_eq!(
2047            cfg.codegen_config.field_attributes,
2048            vec![(".pkg.Msg.f".to_string(), "#[serde(skip)]".to_string())]
2049        );
2050    }
2051
2052    #[test]
2053    fn message_attribute_forwards_normalized_path() {
2054        let cfg = Config::new().message_attribute(".", "#[serde(default)]");
2055        assert_eq!(
2056            cfg.codegen_config.message_attributes,
2057            vec![(".".to_string(), "#[serde(default)]".to_string())]
2058        );
2059    }
2060
2061    #[test]
2062    fn enum_attribute_forwards_normalized_path() {
2063        let cfg = Config::new().enum_attribute("my.pkg.", "#[derive(strum::EnumIter)]");
2064        assert_eq!(
2065            cfg.codegen_config.enum_attributes,
2066            vec![(
2067                ".my.pkg".to_string(),
2068                "#[derive(strum::EnumIter)]".to_string(),
2069            )]
2070        );
2071        // Other attribute lists must remain untouched.
2072        assert!(cfg.codegen_config.type_attributes.is_empty());
2073        assert!(cfg.codegen_config.message_attributes.is_empty());
2074        assert!(cfg.codegen_config.field_attributes.is_empty());
2075    }
2076
2077    #[test]
2078    fn oneof_attribute_forwards_normalized_path() {
2079        let cfg = Config::new().oneof_attribute("my.pkg.Msg.payload.", "#[derive(Hash)]");
2080        assert_eq!(
2081            cfg.codegen_config.oneof_attributes,
2082            vec![(
2083                ".my.pkg.Msg.payload".to_string(),
2084                "#[derive(Hash)]".to_string()
2085            )]
2086        );
2087        // Other attribute lists must remain untouched.
2088        assert!(cfg.codegen_config.type_attributes.is_empty());
2089        assert!(cfg.codegen_config.enum_attributes.is_empty());
2090        assert!(cfg.codegen_config.message_attributes.is_empty());
2091        assert!(cfg.codegen_config.field_attributes.is_empty());
2092    }
2093
2094    #[test]
2095    fn attribute_calls_accumulate_in_insertion_order() {
2096        let cfg = Config::new()
2097            .type_attribute(".", "#[derive(A)]")
2098            .type_attribute(".pkg.M", "#[derive(B)]")
2099            .type_attribute(".", "#[derive(C)]");
2100        let paths: Vec<_> = cfg
2101            .codegen_config
2102            .type_attributes
2103            .iter()
2104            .map(|(_, a)| a.as_str())
2105            .collect();
2106        assert_eq!(paths, vec!["#[derive(A)]", "#[derive(B)]", "#[derive(C)]"]);
2107    }
2108}