buffa_codegen/lib.rs
1//! Shared code generation logic for buffa.
2//!
3//! This crate takes protobuf descriptors (`google.protobuf.FileDescriptorProto`,
4//! decoded from binary `FileDescriptorSet` data) and emits Rust source code
5//! that uses the `buffa` runtime.
6//!
7//! It is used by:
8//! - `protoc-gen-buffa` (protoc plugin)
9//! - `buffa-build` (build.rs integration)
10//!
11//! # Architecture
12//!
13//! The code generator is intentionally decoupled from how descriptors are
14//! obtained. It receives fully-resolved `FileDescriptorProto`s and produces
15//! Rust source strings. This means:
16//!
17//! - It doesn't parse `.proto` files.
18//! - It doesn't invoke `protoc`.
19//! - It doesn't do import resolution or name linking.
20//!
21//! All of that is handled upstream (by protoc, buf, or a future parser).
22
23pub(crate) mod comments;
24pub mod context;
25pub(crate) mod defaults;
26pub(crate) mod enumeration;
27pub(crate) mod extension;
28pub(crate) mod features;
29#[doc(hidden)]
30pub use buffa_descriptor::generated;
31pub mod idents;
32pub(crate) mod impl_message;
33pub(crate) mod impl_text;
34pub(crate) mod imports;
35pub(crate) mod message;
36pub(crate) mod oneof;
37pub(crate) mod view;
38
39use crate::generated::descriptor::FileDescriptorProto;
40use proc_macro2::TokenStream;
41use quote::quote;
42
43/// Result of generating Rust code for a single `.proto` file.
44#[derive(Debug)]
45pub struct GeneratedFile {
46 /// The output file path (e.g., "my_package.rs").
47 pub name: String,
48 /// The generated Rust source code.
49 pub content: String,
50}
51
52/// Configuration for code generation.
53#[derive(Debug, Clone)]
54#[non_exhaustive]
55pub struct CodeGenConfig {
56 /// Whether to generate borrowed view types (`MyMessageView<'a>`) in
57 /// addition to owned types.
58 pub generate_views: bool,
59 /// Whether to preserve unknown fields (default: true).
60 pub preserve_unknown_fields: bool,
61 /// Whether to derive `serde::Serialize` / `serde::Deserialize` on
62 /// generated message structs and enum types, and emit `#[serde(with = "...")]`
63 /// attributes for proto3 JSON's special scalar encodings (int64 as quoted
64 /// string, bytes as base64, etc.).
65 ///
66 /// When this is `true`, the downstream crate must depend on `serde` and
67 /// must enable the `buffa/json` feature for the runtime helpers.
68 ///
69 /// Oneof fields use `#[serde(flatten)]` with custom `Serialize` /
70 /// `Deserialize` impls so that each variant appears as a top-level
71 /// JSON field (proto3 JSON inline oneof encoding).
72 pub generate_json: bool,
73 /// Whether to emit `#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]`
74 /// on generated message structs and enum types.
75 ///
76 /// When this is `true`, the downstream crate must add `arbitrary` as an
77 /// optional dependency and enable the `buffa/arbitrary` feature.
78 pub generate_arbitrary: bool,
79 /// External type path mappings.
80 ///
81 /// Each entry maps a fully-qualified protobuf path prefix (e.g.,
82 /// `".my.common"`) to a Rust module path (e.g., `"::common_protos"`).
83 /// Types under the proto prefix will reference the extern Rust path
84 /// instead of being generated, allowing shared proto packages to be
85 /// compiled once in a dedicated crate and referenced from others.
86 ///
87 /// Well-known types (`google.protobuf.*`) are automatically mapped to
88 /// `::buffa_types::google::protobuf::*` without needing an explicit
89 /// entry here. To override with a custom implementation, add an
90 /// `extern_path` for `.google.protobuf` pointing to your crate.
91 pub extern_paths: Vec<(String, String)>,
92 /// Fully-qualified proto field paths whose `bytes` fields should use
93 /// `bytes::Bytes` instead of `Vec<u8>`.
94 ///
95 /// Each entry is a proto path prefix (e.g., `".my.pkg.MyMessage.data"` for
96 /// a specific field, or `"."` for all bytes fields). The path is matched
97 /// as a prefix, so `"."` applies to every bytes field in every message.
98 pub bytes_fields: Vec<String>,
99 /// Honor `features.utf8_validation = NONE` by emitting `Vec<u8>` / `&[u8]`
100 /// for such string fields instead of `String` / `&str`.
101 ///
102 /// When `false` (the default), buffa emits `String` for all string fields
103 /// and **validates UTF-8 on decode** — stricter than proto2 requires, but
104 /// ergonomic and safe.
105 ///
106 /// When `true`, string fields with `utf8_validation = NONE` (all proto2
107 /// strings by default, and editions fields that opt into `NONE`) become
108 /// `Vec<u8>` / `&[u8]`. Decode skips validation; the caller decides at the
109 /// call site whether to `std::str::from_utf8` (checked) or
110 /// `from_utf8_unchecked` (trusted-input fast path). This is the only
111 /// sound Rust mapping when strings may actually contain non-UTF-8 bytes.
112 ///
113 /// **This is a breaking change for proto2** — enable only for new code or
114 /// when profiling identifies UTF-8 validation as a bottleneck.
115 pub strict_utf8_mapping: bool,
116 /// Permit `option message_set_wire_format = true` on input messages.
117 ///
118 /// MessageSet is a legacy Google-internal wire format that wraps each
119 /// extension in a group structure instead of using regular field tags.
120 /// When `false` (the default), encountering such a message is a codegen
121 /// error — the flag exists to make MessageSet use explicit, since the
122 /// format is obsolete outside of interop with very old Google protos.
123 pub allow_message_set: bool,
124 /// Whether to emit `impl buffa::text::TextFormat` on generated message
125 /// structs for textproto (human-readable text format) encoding/decoding.
126 ///
127 /// When this is `true`, the downstream crate must enable the `buffa/text`
128 /// feature for the runtime encoder/decoder.
129 pub generate_text: bool,
130 /// Whether to emit the file-level `register_types(&mut TypeRegistry)` fn.
131 ///
132 /// Default `true`. Set to `false` when multiple generated files are
133 /// `include!`d into the same namespace (the identically-named fns would
134 /// collide) — e.g. `buffa-types`' WKTs, which hand-roll
135 /// `register_wkt_types` instead. The per-message `__*_JSON_ANY` /
136 /// `__*_TEXT_ANY` consts are still emitted; only the aggregating fn
137 /// is suppressed.
138 pub emit_register_fn: bool,
139}
140
141impl Default for CodeGenConfig {
142 fn default() -> Self {
143 Self {
144 generate_views: true,
145 preserve_unknown_fields: true,
146 generate_json: false,
147 generate_arbitrary: false,
148 extern_paths: Vec::new(),
149 bytes_fields: Vec::new(),
150 strict_utf8_mapping: false,
151 allow_message_set: false,
152 generate_text: false,
153 emit_register_fn: true,
154 }
155 }
156}
157
158/// Compute the effective extern path list by starting with user-provided
159/// mappings and adding the default WKT mapping if appropriate.
160///
161/// The default mapping `".google.protobuf" → "::buffa_types::google::protobuf"`
162/// is added unless:
163/// - The user already provided an extern_path covering `.google.protobuf`
164/// - Any of the files being generated are in the `google.protobuf` package
165/// (i.e., we're building `buffa-types` itself)
166pub(crate) fn effective_extern_paths(
167 file_descriptors: &[FileDescriptorProto],
168 files_to_generate: &[String],
169 config: &CodeGenConfig,
170) -> Vec<(String, String)> {
171 let mut paths = config.extern_paths.clone();
172
173 // Only an EXACT .google.protobuf mapping suppresses auto-injection.
174 // A sub-package mapping like .google.protobuf.compiler does NOT cover
175 // WKTs like Timestamp — resolve_extern_prefix's longest-prefix matching
176 // lets both coexist, so we still inject the parent mapping.
177 let has_wkt_mapping = paths.iter().any(|(proto, _)| proto == ".google.protobuf");
178
179 if !has_wkt_mapping {
180 // Check if we're generating google.protobuf files ourselves
181 // (e.g., building buffa-types). If so, don't auto-map.
182 let generating_wkts = file_descriptors
183 .iter()
184 .filter(|fd| {
185 fd.name
186 .as_deref()
187 .is_some_and(|n| files_to_generate.iter().any(|f| f == n))
188 })
189 .any(|fd| fd.package.as_deref() == Some("google.protobuf"));
190
191 if !generating_wkts {
192 paths.push((
193 ".google.protobuf".to_string(),
194 "::buffa_types::google::protobuf".to_string(),
195 ));
196 }
197 }
198
199 paths
200}
201
202/// Generate Rust source files from a set of file descriptors.
203///
204/// `files_to_generate` is the set of file names that were explicitly requested
205/// (matching `CodeGeneratorRequest.file_to_generate`). Descriptors for
206/// dependencies may be present in `file_descriptors` but won't produce output
207/// files unless they appear in `files_to_generate`.
208pub fn generate(
209 file_descriptors: &[FileDescriptorProto],
210 files_to_generate: &[String],
211 config: &CodeGenConfig,
212) -> Result<Vec<GeneratedFile>, CodeGenError> {
213 let ctx = context::CodeGenContext::for_generate(file_descriptors, files_to_generate, config);
214
215 let mut output = Vec::new();
216 for file_name in files_to_generate {
217 let file_desc = file_descriptors
218 .iter()
219 .find(|f| f.name.as_deref() == Some(file_name.as_str()))
220 .ok_or_else(|| CodeGenError::FileNotFound(file_name.clone()))?;
221
222 let content = generate_file(&ctx, file_desc)?;
223 let rust_filename = proto_path_to_rust_module(file_name);
224 output.push(GeneratedFile {
225 name: rust_filename,
226 content,
227 });
228 }
229
230 Ok(output)
231}
232
233/// Generate a module tree that assembles generated `.rs` files into
234/// nested `pub mod` blocks matching the protobuf package hierarchy.
235///
236/// Each entry is a `(file_name, package)` pair where `package` is the
237/// dot-separated protobuf package name (e.g., `"google.api"`). The module
238/// tree is built from the **package** hierarchy so that `super::`-based
239/// cross-package references resolve correctly.
240///
241/// `include_prefix` is prepended to file names in `include!` directives.
242/// Use `""` for relative paths or `concat!(env!("OUT_DIR"), "/")` style
243/// for build.rs output.
244///
245/// When `emit_inner_allow` is true, a `#![allow(...)]` inner attribute is
246/// emitted at the top of the file. This is appropriate when the output is
247/// used directly as a module file (e.g., `mod.rs`) but NOT when the output
248/// is consumed via `include!` (inner attributes are not valid in that
249/// context).
250pub fn generate_module_tree(
251 entries: &[(&str, &str)],
252 include_prefix: &str,
253 emit_inner_allow: bool,
254) -> String {
255 use std::collections::BTreeMap;
256 use std::fmt::Write;
257
258 use crate::idents::escape_mod_ident;
259
260 #[derive(Default)]
261 struct ModNode {
262 files: Vec<String>,
263 children: BTreeMap<String, Self>,
264 }
265
266 let mut root = ModNode::default();
267
268 for (file_name, package) in entries {
269 let pkg_parts: Vec<&str> = if package.is_empty() {
270 vec![]
271 } else {
272 package.split('.').collect()
273 };
274
275 let mut node = &mut root;
276 for seg in &pkg_parts {
277 node = node.children.entry(seg.to_string()).or_default();
278 }
279 node.files.push(file_name.to_string());
280 }
281
282 let mut out = String::new();
283 writeln!(out, "// @generated by buffa. DO NOT EDIT.").unwrap();
284 const ALLOW_LINTS: &str = "non_camel_case_types, dead_code, unused_imports, \
285 clippy::derivable_impls, clippy::match_single_binding, \
286 clippy::uninlined_format_args, clippy::doc_lazy_continuation";
287
288 if emit_inner_allow {
289 writeln!(out, "#![allow({ALLOW_LINTS})]").unwrap();
290 }
291 writeln!(out).unwrap();
292
293 fn emit(out: &mut String, node: &ModNode, depth: usize, prefix: &str, lints: &str) {
294 let indent = " ".repeat(depth);
295
296 for file in &node.files {
297 writeln!(out, r#"{indent}include!("{prefix}{file}");"#).unwrap();
298 }
299
300 for (name, child) in &node.children {
301 let escaped = escape_mod_ident(name);
302 writeln!(out, "{indent}#[allow({lints})]").unwrap();
303 writeln!(out, "{indent}pub mod {escaped} {{").unwrap();
304 writeln!(out, "{indent} use super::*;").unwrap();
305 emit(out, child, depth + 1, prefix, lints);
306 writeln!(out, "{indent}}}").unwrap();
307 }
308 }
309
310 emit(&mut out, &root, 0, include_prefix, ALLOW_LINTS);
311 out
312}
313
314/// Check that no fields in the file use the `__buffa_` reserved prefix.
315fn check_reserved_field_names(file: &FileDescriptorProto) -> Result<(), CodeGenError> {
316 fn check_message(
317 msg: &crate::generated::descriptor::DescriptorProto,
318 parent_name: &str,
319 ) -> Result<(), CodeGenError> {
320 let msg_name = msg.name.as_deref().unwrap_or("");
321 let fqn = if parent_name.is_empty() {
322 msg_name.to_string()
323 } else {
324 format!("{}.{}", parent_name, msg_name)
325 };
326
327 for field in &msg.field {
328 if let Some(name) = &field.name {
329 if name.starts_with("__buffa_") {
330 return Err(CodeGenError::ReservedFieldName {
331 message_name: fqn,
332 field_name: name.clone(),
333 });
334 }
335 }
336 }
337
338 for nested in &msg.nested_type {
339 check_message(nested, &fqn)?;
340 }
341
342 Ok(())
343 }
344
345 let package = file.package.as_deref().unwrap_or("");
346 for msg in &file.message_type {
347 check_message(msg, package)?;
348 }
349 Ok(())
350}
351
352/// Check that no sibling messages produce the same snake_case module name.
353///
354/// For example, `HTTPRequest` and `HttpRequest` both produce
355/// `pub mod http_request`, which would be a compile error.
356fn check_module_name_conflicts(file: &FileDescriptorProto) -> Result<(), CodeGenError> {
357 use std::collections::HashMap;
358
359 fn check_siblings(
360 messages: &[crate::generated::descriptor::DescriptorProto],
361 scope: &str,
362 ) -> Result<(), CodeGenError> {
363 // Map from snake_case module name → original proto name.
364 let mut seen: HashMap<String, &str> = HashMap::new();
365
366 for msg in messages {
367 let name = msg.name.as_deref().unwrap_or("");
368 let module_name = crate::oneof::to_snake_case(name);
369
370 if let Some(existing) = seen.get(&module_name) {
371 return Err(CodeGenError::ModuleNameConflict {
372 scope: scope.to_string(),
373 name_a: existing.to_string(),
374 name_b: name.to_string(),
375 module_name,
376 });
377 }
378 seen.insert(module_name, name);
379
380 // Recurse into nested messages.
381 let child_scope = if scope.is_empty() {
382 name.to_string()
383 } else {
384 format!("{}.{}", scope, name)
385 };
386 check_siblings(&msg.nested_type, &child_scope)?;
387 }
388
389 Ok(())
390 }
391
392 let package = file.package.as_deref().unwrap_or("");
393 check_siblings(&file.message_type, package)
394}
395
396/// Check that nested type names don't collide with oneof enum names.
397///
398/// Nested messages/enums and oneof enums coexist in the message's module.
399/// A nested `message MyField` and `oneof my_field` both produce `MyField`.
400fn check_nested_type_oneof_conflicts(file: &FileDescriptorProto) -> Result<(), CodeGenError> {
401 use std::collections::HashSet;
402
403 fn check_message(
404 msg: &crate::generated::descriptor::DescriptorProto,
405 scope: &str,
406 ) -> Result<(), CodeGenError> {
407 let msg_name = msg.name.as_deref().unwrap_or("");
408 let fqn = if scope.is_empty() {
409 msg_name.to_string()
410 } else {
411 format!("{}.{}", scope, msg_name)
412 };
413
414 // Collect names that nested types/enums will occupy in the module.
415 let mut nested_names: HashSet<&str> = HashSet::new();
416 for nested in &msg.nested_type {
417 if let Some(name) = &nested.name {
418 nested_names.insert(name);
419 }
420 }
421 for nested_enum in &msg.enum_type {
422 if let Some(name) = &nested_enum.name {
423 nested_names.insert(name);
424 }
425 }
426
427 // Check each non-synthetic oneof's PascalCase name against nested
428 // type names. Synthetic oneofs (created by proto3 `optional` fields)
429 // never produce a Rust enum, so they cannot collide.
430 for (idx, oneof) in msg.oneof_decl.iter().enumerate() {
431 let has_real_fields = msg.field.iter().any(|f| {
432 crate::impl_message::is_real_oneof_member(f) && f.oneof_index == Some(idx as i32)
433 });
434 if !has_real_fields {
435 continue;
436 }
437 if let Some(oneof_name) = &oneof.name {
438 let rust_name = crate::oneof::to_pascal_case(oneof_name);
439 if nested_names.contains(rust_name.as_str()) {
440 return Err(CodeGenError::NestedTypeOneofConflict {
441 scope: fqn,
442 nested_name: rust_name.clone(),
443 oneof_name: oneof_name.clone(),
444 rust_name,
445 });
446 }
447 }
448 }
449
450 // Recurse into nested messages.
451 for nested in &msg.nested_type {
452 check_message(nested, &fqn)?;
453 }
454
455 Ok(())
456 }
457
458 let package = file.package.as_deref().unwrap_or("");
459 for msg in &file.message_type {
460 check_message(msg, package)?;
461 }
462 Ok(())
463}
464
465/// Check that no message named `FooView` collides with the generated view
466/// type for a sibling message `Foo`.
467fn check_view_name_conflicts(file: &FileDescriptorProto) -> Result<(), CodeGenError> {
468 use std::collections::HashSet;
469
470 fn check_siblings(
471 messages: &[crate::generated::descriptor::DescriptorProto],
472 scope: &str,
473 ) -> Result<(), CodeGenError> {
474 // Collect all message names at this level.
475 let names: HashSet<&str> = messages.iter().filter_map(|m| m.name.as_deref()).collect();
476
477 // For each message Foo, check if FooView also exists.
478 for msg in messages {
479 let name = msg.name.as_deref().unwrap_or("");
480 let view_name = format!("{}View", name);
481 if names.contains(view_name.as_str()) {
482 return Err(CodeGenError::ViewNameConflict {
483 scope: scope.to_string(),
484 owned_msg: name.to_string(),
485 view_msg: view_name,
486 });
487 }
488 }
489
490 // Recurse into nested messages.
491 for msg in messages {
492 let name = msg.name.as_deref().unwrap_or("");
493 let child_scope = if scope.is_empty() {
494 name.to_string()
495 } else {
496 format!("{}.{}", scope, name)
497 };
498 check_siblings(&msg.nested_type, &child_scope)?;
499 }
500
501 Ok(())
502 }
503
504 let package = file.package.as_deref().unwrap_or("");
505 check_siblings(&file.message_type, package)
506}
507
508/// Generate Rust source for a single `.proto` file.
509fn generate_file(
510 ctx: &context::CodeGenContext,
511 file: &FileDescriptorProto,
512) -> Result<String, CodeGenError> {
513 // Validate descriptors before generating code.
514 check_reserved_field_names(file)?;
515 check_module_name_conflicts(file)?;
516 check_nested_type_oneof_conflicts(file)?;
517 if ctx.config.generate_views {
518 check_view_name_conflicts(file)?;
519 }
520
521 let resolver = imports::ImportResolver::for_file(file);
522 let mut tokens = resolver.generate_use_block();
523 let current_package = file.package.as_deref().unwrap_or("");
524 let features = crate::features::for_file(file);
525 for enum_type in &file.enum_type {
526 let enum_rust_name = enum_type.name.as_deref().unwrap_or("");
527 let enum_fqn = if current_package.is_empty() {
528 enum_rust_name.to_string()
529 } else {
530 format!("{}.{}", current_package, enum_rust_name)
531 };
532 tokens.extend(enumeration::generate_enum(
533 ctx,
534 enum_type,
535 enum_rust_name,
536 &enum_fqn,
537 &features,
538 &resolver,
539 )?);
540 }
541 // Collect paths to registry consts (both file-level and nested-in-message)
542 // for the optional `register_types` fn below. JSON/text are tracked
543 // separately so each registration line is emitted only under its
544 // corresponding `generate_*` flag.
545 let mut reg = message::RegistryPaths::default();
546
547 for message_type in &file.message_type {
548 let top_level_name = message_type.name.as_deref().unwrap_or("");
549 let proto_fqn = if current_package.is_empty() {
550 top_level_name.to_string()
551 } else {
552 format!("{}.{}", current_package, top_level_name)
553 };
554 let (msg_top, msg_mod, msg_reg) = message::generate_message(
555 ctx,
556 message_type,
557 current_package,
558 top_level_name,
559 &proto_fqn,
560 &features,
561 &resolver,
562 )?;
563 tokens.extend(msg_top);
564 // Nested extension const paths are relative to the message's module
565 // scope; prefix with `<mod_ident>::` for the package-level view.
566 let mod_name = crate::oneof::to_snake_case(top_level_name);
567 let mod_ident = crate::message::make_field_ident(&mod_name);
568 for p in msg_reg.json_ext {
569 reg.json_ext.push(quote! { #mod_ident :: #p });
570 }
571 for p in msg_reg.text_ext {
572 reg.text_ext.push(quote! { #mod_ident :: #p });
573 }
574 // Any-entry paths are already relative to the struct's scope
575 // (= file scope for top-level messages) — no prefix needed.
576 reg.json_any.extend(msg_reg.json_any);
577 reg.text_any.extend(msg_reg.text_any);
578
579 let view_mod = if ctx.config.generate_views {
580 let (view_top, view_mod) = view::generate_view(
581 ctx,
582 message_type,
583 current_package,
584 top_level_name,
585 &proto_fqn,
586 &features,
587 )?;
588 tokens.extend(view_top);
589 view_mod
590 } else {
591 TokenStream::new()
592 };
593
594 // Combine message and view module items into a single `pub mod`.
595 if !msg_mod.is_empty() || !view_mod.is_empty() {
596 tokens.extend(quote! {
597 pub mod #mod_ident {
598 #[allow(unused_imports)]
599 use super::*;
600 #msg_mod
601 #view_mod
602 }
603 });
604 }
605 }
606
607 let (file_ext_tokens, file_ext_json, file_ext_text) = extension::generate_extensions(
608 ctx,
609 &file.extension,
610 current_package,
611 0,
612 &features,
613 current_package,
614 )?;
615 tokens.extend(file_ext_tokens);
616 for id in file_ext_json {
617 reg.json_ext.push(quote! { #id });
618 }
619 for id in file_ext_text {
620 reg.text_ext.push(quote! { #id });
621 }
622
623 // `register_types(&mut TypeRegistry)` — one call per entry, split by
624 // format. Only emitted when at least one entry exists. Lines are
625 // gated at codegen time by `generate_json` / `generate_text`; the
626 // corresponding `register_*` methods on `TypeRegistry` are feature-gated
627 // in buffa, so a flag/feature mismatch surfaces as a compile error.
628 if ctx.config.emit_register_fn && !reg.is_empty() {
629 let json_any = ®.json_any;
630 let json_ext = ®.json_ext;
631 let text_any = ®.text_any;
632 let text_ext = ®.text_ext;
633 tokens.extend(quote! {
634 /// Register this file's `Any` type entries and extension entries
635 /// (JSON and/or text, per codegen config) with the given registry.
636 pub fn register_types(reg: &mut ::buffa::type_registry::TypeRegistry) {
637 #( reg.register_json_any(#json_any); )*
638 #( reg.register_json_ext(#json_ext); )*
639 #( reg.register_text_any(#text_any); )*
640 #( reg.register_text_ext(#text_ext); )*
641 }
642 });
643 }
644
645 // Parse the token stream into a syn::File and format with prettyplease.
646 // Regular `//` comments cannot appear inside quote! blocks, so the file
647 // header is prepended as a raw string after formatting.
648 let syntax_tree =
649 syn::parse2::<syn::File>(tokens).map_err(|e| CodeGenError::InvalidSyntax(e.to_string()))?;
650 let formatted = prettyplease::unparse(&syntax_tree);
651
652 let source_line = file
653 .name
654 .as_ref()
655 .map_or(String::new(), |n| format!("// source: {n}\n"));
656
657 Ok(format!(
658 "// @generated by protoc-gen-buffa. DO NOT EDIT.\n{source_line}\n{formatted}"
659 ))
660}
661
662/// Convert a `.proto` file path to a Rust module file name.
663///
664/// e.g., "google/protobuf/timestamp.proto" → "google.protobuf.timestamp.rs"
665/// Convert a proto file path to a generated Rust file name.
666///
667/// e.g., `"google/protobuf/timestamp.proto"` → `"google.protobuf.timestamp.rs"`
668pub fn proto_path_to_rust_module(proto_path: &str) -> String {
669 let without_ext = proto_path.strip_suffix(".proto").unwrap_or(proto_path);
670 format!("{}.rs", without_ext.replace('/', "."))
671}
672
673/// Code generation error.
674#[derive(Debug, Clone, thiserror::Error)]
675#[non_exhaustive]
676pub enum CodeGenError {
677 /// A required field was absent in a descriptor.
678 ///
679 /// The `&'static str` names the missing field for diagnostics.
680 #[error("missing required descriptor field: {0}")]
681 MissingField(&'static str),
682 /// A resolved type path string could not be parsed as a Rust type.
683 #[error("invalid Rust type path: '{0}'")]
684 InvalidTypePath(String),
685 /// The accumulated `TokenStream` failed to parse as valid Rust syntax.
686 #[error("generated code failed to parse as Rust: {0}")]
687 InvalidSyntax(String),
688 /// A requested file was not present in the descriptor set.
689 #[error("file_to_generate '{0}' not found in descriptor set")]
690 FileNotFound(String),
691 /// Unexpected descriptor state (e.g. a map entry or oneof that cannot be
692 /// resolved to a known descriptor field).
693 #[error("codegen error: {0}")]
694 Other(String),
695 /// A proto field name uses the `__buffa_` reserved prefix, which would
696 /// conflict with buffa's internal generated fields.
697 #[error(
698 "reserved field name '{field_name}' in message '{message_name}': \
699 proto field names starting with '__buffa_' conflict with buffa's \
700 internal fields"
701 )]
702 ReservedFieldName {
703 message_name: String,
704 field_name: String,
705 },
706 /// Two sibling messages produce the same Rust module name after
707 /// snake_case conversion (e.g., `HTTPRequest` and `HttpRequest` both
708 /// become `pub mod http_request`).
709 #[error(
710 "module name conflict in '{scope}': messages '{name_a}' and '{name_b}' \
711 both produce module '{module_name}'"
712 )]
713 ModuleNameConflict {
714 scope: String,
715 name_a: String,
716 name_b: String,
717 module_name: String,
718 },
719 /// A nested message/enum name collides with a oneof enum name inside
720 /// the same message module.
721 #[error(
722 "name conflict in '{scope}': nested type '{nested_name}' and \
723 oneof '{oneof_name}' both produce '{rust_name}' in the message module"
724 )]
725 NestedTypeOneofConflict {
726 scope: String,
727 nested_name: String,
728 oneof_name: String,
729 rust_name: String,
730 },
731 /// A message named `FooView` collides with the generated view type for
732 /// message `Foo`.
733 #[error(
734 "name conflict in '{scope}': message '{view_msg}' collides with \
735 the generated view type for message '{owned_msg}'"
736 )]
737 ViewNameConflict {
738 scope: String,
739 owned_msg: String,
740 view_msg: String,
741 },
742 /// The input contains a message with `option message_set_wire_format = true`
743 /// but [`CodeGenConfig::allow_message_set`] was not set.
744 #[error(
745 "message '{message_name}' uses `option message_set_wire_format = true` \
746 but CodeGenConfig::allow_message_set is false; MessageSet is a legacy \
747 wire format — set allow_message_set(true) if this is intentional"
748 )]
749 MessageSetNotSupported { message_name: String },
750}
751
752#[cfg(test)]
753mod tests;