Skip to main content

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 = &reg.json_any;
630        let json_ext = &reg.json_ext;
631        let text_any = &reg.text_any;
632        let text_ext = &reg.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;