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