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}