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