Skip to content

Commit e44d64a

Browse files
committed
feat(prost-build): add type_name_suffix and type_name_prefix configuration
Add ability to specify global suffix and prefix for generate protobuf types, allowing users to transform generated type names (e.g., `MyMessage` -> `MyMessageProto`) - Add type_name_suffix() and type_name_prefix() methods to Config - Apply affixes to messages, enums, and oneof types during code generation - Preserve well-known types from prost_types (no suffix/prefix applied) - Support combined prefix and suffix usage - Add comprehensive test coverage including import scenarios Examples: ```rust prost_build::Config::new() .type_name_suffix("Proto") // MyMessage -> MyMessageProto .type_name_prefix("Proto") // MyMessage -> ProtoMyMessage .compile_protos(...) ```
1 parent df814bb commit e44d64a

13 files changed

Lines changed: 621 additions & 18 deletions

prost-build/src/code_generator.rs

Lines changed: 69 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
use std::ascii;
22
use std::borrow::Cow;
33
use std::collections::{HashMap, HashSet};
4-
use std::iter;
54

65
use itertools::{Either, Itertools};
76
use log::debug;
@@ -92,14 +91,6 @@ impl OneofField {
9291
fn rust_name(&self) -> String {
9392
to_snake(self.descriptor.name())
9493
}
95-
96-
fn type_name(&self) -> String {
97-
let mut name = to_upper_camel(self.descriptor.name());
98-
if self.has_type_name_conflict {
99-
name.push_str("OneOf");
100-
}
101-
name
102-
}
10394
}
10495

10596
impl<'b> CodeGenerator<'_, 'b> {
@@ -260,7 +251,8 @@ impl<'b> CodeGenerator<'_, 'b> {
260251
self.append_skip_debug(&fq_message_name);
261252
self.push_indent();
262253
self.buf.push_str("pub struct ");
263-
self.buf.push_str(&to_upper_camel(&message_name));
254+
self.buf
255+
.push_str(&self.type_name_with_affixes(&message_name));
264256
self.buf.push_str(" {\n");
265257

266258
self.depth += 1;
@@ -327,7 +319,7 @@ impl<'b> CodeGenerator<'_, 'b> {
327319

328320
self.buf.push_str(&format!(
329321
"impl {prost_path}::Name for {} {{\n",
330-
to_upper_camel(message_name)
322+
self.type_name_with_affixes(message_name)
331323
));
332324
self.depth += 1;
333325

@@ -579,7 +571,13 @@ impl<'b> CodeGenerator<'_, 'b> {
579571
fq_message_name: &str,
580572
oneof: &OneofField,
581573
) {
582-
let type_name = format!("{}::{}", to_snake(message_name), oneof.type_name());
574+
// Apply OneOf suffix if there's a conflict, then apply global affixes
575+
let mut base_name = oneof.descriptor.name().to_string();
576+
if oneof.has_type_name_conflict {
577+
base_name.push_str("_one_of");
578+
}
579+
let oneof_type_name = self.type_name_with_affixes(&base_name);
580+
let type_name = format!("{}::{}", to_snake(message_name), oneof_type_name);
583581
self.append_doc(fq_message_name, None);
584582
self.push_indent();
585583
self.buf.push_str(&format!(
@@ -633,7 +631,14 @@ impl<'b> CodeGenerator<'_, 'b> {
633631
self.append_skip_debug(fq_message_name);
634632
self.push_indent();
635633
self.buf.push_str("pub enum ");
636-
self.buf.push_str(&oneof.type_name());
634+
635+
// Apply OneOf suffix if there's a conflict, then apply global affixes
636+
let mut base_name = oneof.descriptor.name().to_string();
637+
if oneof.has_type_name_conflict {
638+
base_name.push_str("_one_of");
639+
}
640+
let oneof_type_name = self.type_name_with_affixes(&base_name);
641+
self.buf.push_str(&oneof_type_name);
637642
self.buf.push_str(" {\n");
638643

639644
self.path.push(2);
@@ -710,10 +715,10 @@ impl<'b> CodeGenerator<'_, 'b> {
710715
debug!(" enum: {:?}", desc.name());
711716

712717
let proto_enum_name = desc.name();
713-
let enum_name = to_upper_camel(proto_enum_name);
718+
let fq_proto_enum_name = self.fq_name(proto_enum_name);
719+
let enum_name = self.type_name_with_affixes(proto_enum_name);
714720

715721
let enum_values = &desc.value;
716-
let fq_proto_enum_name = self.fq_name(proto_enum_name);
717722

718723
if self
719724
.context
@@ -995,11 +1000,21 @@ impl<'b> CodeGenerator<'_, 'b> {
9951000
ident_path.next();
9961001
}
9971002

998-
local_path
1003+
// Build the base path without the type name
1004+
let base_path: Vec<String> = local_path
9991005
.map(|_| "super".to_string())
10001006
.chain(ident_path.map(to_snake))
1001-
.chain(iter::once(to_upper_camel(ident_type)))
1002-
.join("::")
1007+
.collect();
1008+
1009+
// Apply prefix/suffix to the type name if configured, using the protobuf identifier to determine package
1010+
let type_name = self.type_name_with_affixes_for_package(ident_type, Some(pb_ident));
1011+
1012+
// Join the path with the potentially suffixed type name
1013+
if base_path.is_empty() {
1014+
type_name
1015+
} else {
1016+
format!("{}::{}", base_path.join("::"), type_name)
1017+
}
10031018
}
10041019

10051020
fn field_type_tag(&self, field: &FieldDescriptorProto) -> Cow<'static, str> {
@@ -1069,6 +1084,42 @@ impl<'b> CodeGenerator<'_, 'b> {
10691084
message_name,
10701085
)
10711086
}
1087+
1088+
/// Return the type name with optional prefix and/or suffix based on configuration
1089+
fn type_name_with_affixes(&self, type_name: &str) -> String {
1090+
self.type_name_with_affixes_for_package(type_name, None)
1091+
}
1092+
1093+
fn type_name_with_affixes_for_package(
1094+
&self,
1095+
type_name: &str,
1096+
pb_ident: Option<&str>,
1097+
) -> String {
1098+
let mut type_name = to_upper_camel(type_name);
1099+
1100+
// Determine the lookup path for PathMap
1101+
let lookup_path = match pb_ident {
1102+
Some(ident) if ident.starts_with('.') => ident.to_string(),
1103+
_ => {
1104+
if self.package.is_empty() {
1105+
".".to_string()
1106+
} else {
1107+
format!(".{}", self.package.trim_matches('.'))
1108+
}
1109+
}
1110+
};
1111+
1112+
// Let PathMap handle the complex path matching and fallback logic
1113+
if let Some(prefix) = self.config().type_name_prefixes.get_first(&lookup_path) {
1114+
type_name.insert_str(0, prefix);
1115+
}
1116+
1117+
if let Some(suffix) = self.config().type_name_suffixes.get_first(&lookup_path) {
1118+
type_name.push_str(suffix);
1119+
}
1120+
1121+
type_name
1122+
}
10721123
}
10731124

10741125
/// Returns `true` if the repeated field type can be packed.

prost-build/src/config.rs

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ pub struct Config {
5252
pub(crate) skip_source_info: bool,
5353
pub(crate) include_file: Option<PathBuf>,
5454
pub(crate) prost_path: Option<String>,
55+
pub(crate) type_name_prefixes: PathMap<String>,
56+
pub(crate) type_name_suffixes: PathMap<String>,
5557
#[cfg(feature = "format")]
5658
pub(crate) fmt: bool,
5759
}
@@ -353,6 +355,134 @@ impl Config {
353355
self
354356
}
355357

358+
/// Add a suffix to all generated type names.
359+
///
360+
/// # Arguments
361+
///
362+
/// **`suffix`** - an arbitrary string to be appended to all type names. For example,
363+
/// "Proto" would change `MyMessage` to `MyMessageProto`.
364+
///
365+
/// # Examples
366+
///
367+
/// ```rust
368+
/// # let mut config = prost_build::Config::new();
369+
/// // Add "Proto" suffix to all generated types
370+
/// config.type_name_suffix("Proto");
371+
/// ```
372+
pub fn type_name_suffix<S>(&mut self, suffix: S) -> &mut Self
373+
where
374+
S: AsRef<str>,
375+
{
376+
self.type_name_suffixes
377+
.insert(".".to_string(), suffix.as_ref().to_string());
378+
self
379+
}
380+
381+
/// Add a prefix to all generated type names.
382+
///
383+
/// # Arguments
384+
///
385+
/// **`prefix`** - an arbitrary string to be prepended to all type names. For example,
386+
/// "Proto" would change `MyMessage` to `ProtoMyMessage`.
387+
///
388+
/// # Examples
389+
///
390+
/// ```rust
391+
/// # let mut config = prost_build::Config::new();
392+
/// // Add "Proto" prefix to all generated types
393+
/// config.type_name_prefix("Proto");
394+
/// ```
395+
pub fn type_name_prefix<P>(&mut self, prefix: P) -> &mut Self
396+
where
397+
P: AsRef<str>,
398+
{
399+
self.type_name_prefixes
400+
.insert(".".to_string(), prefix.as_ref().to_string());
401+
self
402+
}
403+
404+
/// Configure package-specific type name suffixes.
405+
///
406+
/// This allows different suffix settings for different proto packages,
407+
/// which is especially useful for cross-crate compatibility when importing
408+
/// proto files from external crates.
409+
///
410+
/// # Arguments
411+
///
412+
/// **`paths`** - package paths with their desired suffix. Paths starting with '.'
413+
/// are treated as fully-qualified package names. Paths without a leading '.' are
414+
/// treated as relative and suffix-matched.
415+
///
416+
/// # Examples
417+
///
418+
/// ```rust
419+
/// # let mut config = prost_build::Config::new();
420+
/// // Apply "Proto" suffix to a specific package
421+
/// config.package_type_name_suffix([(".my_package", "Proto")]);
422+
///
423+
/// // Apply different suffixes to different packages
424+
/// config.package_type_name_suffix([
425+
/// (".external_api", "External"), // external API types
426+
/// (".internal", "Internal"), // internal types
427+
/// ]);
428+
///
429+
/// // Apply suffix to all packages under a namespace
430+
/// config.package_type_name_suffix([("my_company", "Pb")]);
431+
/// ```
432+
pub fn package_type_name_suffix<I, P, S>(&mut self, paths: I) -> &mut Self
433+
where
434+
I: IntoIterator<Item = (P, S)>,
435+
P: AsRef<str>,
436+
S: AsRef<str>,
437+
{
438+
for (path, suffix) in paths {
439+
self.type_name_suffixes
440+
.insert(path.as_ref().to_string(), suffix.as_ref().to_string());
441+
}
442+
self
443+
}
444+
445+
/// Configure package-specific type name prefixes.
446+
///
447+
/// This allows different prefix settings for different proto packages,
448+
/// which is especially useful for cross-crate compatibility when importing
449+
/// proto files from external crates.
450+
///
451+
/// # Arguments
452+
///
453+
/// **`paths`** - package paths with their desired prefix. Paths starting with '.'
454+
/// are treated as fully-qualified package names. Paths without a leading '.' are
455+
/// treated as relative and suffix-matched.
456+
///
457+
/// # Examples
458+
///
459+
/// ```rust
460+
/// # let mut config = prost_build::Config::new();
461+
/// // Apply "Proto" prefix to a specific package
462+
/// config.package_type_name_prefix([(".my_package", "Proto")]);
463+
///
464+
/// // Apply different prefixes to different packages
465+
/// config.package_type_name_prefix([
466+
/// (".external_api", "External"), // external API types
467+
/// (".internal", "Internal"), // internal types
468+
/// ]);
469+
///
470+
/// // Apply prefix to all packages under a namespace
471+
/// config.package_type_name_prefix([("my_company", "Pb")]);
472+
/// ```
473+
pub fn package_type_name_prefix<I, P, S>(&mut self, paths: I) -> &mut Self
474+
where
475+
I: IntoIterator<Item = (P, S)>,
476+
P: AsRef<str>,
477+
S: AsRef<str>,
478+
{
479+
for (path, prefix) in paths {
480+
self.type_name_prefixes
481+
.insert(path.as_ref().to_string(), prefix.as_ref().to_string());
482+
}
483+
self
484+
}
485+
356486
/// Wrap matched fields in a `Box`.
357487
///
358488
/// # Arguments
@@ -1193,6 +1323,8 @@ impl default::Default for Config {
11931323
skip_source_info: false,
11941324
include_file: None,
11951325
prost_path: None,
1326+
type_name_prefixes: PathMap::default(),
1327+
type_name_suffixes: PathMap::default(),
11961328
#[cfg(feature = "format")]
11971329
fmt: true,
11981330
}
@@ -1219,6 +1351,8 @@ impl fmt::Debug for Config {
12191351
.field("disable_comments", &self.disable_comments)
12201352
.field("skip_debug", &self.skip_debug)
12211353
.field("prost_path", &self.prost_path)
1354+
.field("type_name_prefixes", &self.type_name_prefixes)
1355+
.field("type_name_suffixes", &self.type_name_suffixes)
12221356
.finish()
12231357
}
12241358
}

tests/build.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,41 @@ fn main() {
184184
.compile_protos(&[src.join("oneof_name_conflict.proto")], includes)
185185
.unwrap();
186186

187+
// Test type name suffix
188+
prost_build::Config::new()
189+
.type_name_suffix("Proto")
190+
.compile_protos(&[src.join("type_name_suffix.proto")], includes)
191+
.unwrap();
192+
193+
// Test type name prefix
194+
prost_build::Config::new()
195+
.type_name_prefix("Proto")
196+
.compile_protos(&[src.join("type_name_prefix.proto")], includes)
197+
.unwrap();
198+
199+
// Test type name prefix and suffix together
200+
prost_build::Config::new()
201+
.type_name_prefix("Pre")
202+
.type_name_suffix("Post")
203+
.compile_protos(&[src.join("type_name_prefix_suffix.proto")], includes)
204+
.unwrap();
205+
206+
// Test type name suffix with imports (well-known types) and package-specific suffixes
207+
prost_build::Config::new()
208+
.type_name_suffix("Proto")
209+
.package_type_name_suffix([
210+
// Different suffix for external package
211+
(".external_package", "ABC"),
212+
])
213+
.compile_protos(
214+
&[
215+
src.join("type_name_imports.proto"),
216+
src.join("type_name_external_package.proto"),
217+
],
218+
&[src.clone(), src.join("include")],
219+
)
220+
.unwrap();
221+
187222
// Check that attempting to compile a .proto without a package declaration does not result in an error.
188223
config
189224
.compile_protos(&[src.join("no_package.proto")], includes)

tests/src/lib.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,18 @@ mod ident_conversion;
8787
#[cfg(test)]
8888
mod oneof_name_conflict;
8989

90+
#[cfg(test)]
91+
mod type_name_suffix;
92+
93+
#[cfg(test)]
94+
mod type_name_prefix;
95+
96+
#[cfg(test)]
97+
mod type_name_prefix_suffix;
98+
99+
#[cfg(test)]
100+
mod type_name_imports;
101+
90102
mod test_enum_named_option_value {
91103
include!(concat!(env!("OUT_DIR"), "/myenum.optionn.rs"));
92104
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
syntax = "proto3";
2+
3+
package external_package;
4+
5+
message ExternalMessage {
6+
string name = 1;
7+
int32 value = 2;
8+
}
9+
10+
enum ExternalStatus {
11+
EXTERNAL_UNKNOWN = 0;
12+
EXTERNAL_ACTIVE = 1;
13+
}

0 commit comments

Comments
 (0)