Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 69 additions & 18 deletions prost-build/src/code_generator.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
use std::ascii;
use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::iter;

use itertools::{Either, Itertools};
use log::debug;
Expand Down Expand Up @@ -92,14 +91,6 @@ impl OneofField {
fn rust_name(&self) -> String {
to_snake(self.descriptor.name())
}

fn type_name(&self) -> String {
let mut name = to_upper_camel(self.descriptor.name());
if self.has_type_name_conflict {
name.push_str("OneOf");
}
name
}
}

impl<'b> CodeGenerator<'_, 'b> {
Expand Down Expand Up @@ -260,7 +251,8 @@ impl<'b> CodeGenerator<'_, 'b> {
self.append_skip_debug(&fq_message_name);
self.push_indent();
self.buf.push_str("pub struct ");
self.buf.push_str(&to_upper_camel(&message_name));
self.buf
.push_str(&self.type_name_with_affixes(&message_name));
self.buf.push_str(" {\n");

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

self.buf.push_str(&format!(
"impl {prost_path}::Name for {} {{\n",
to_upper_camel(message_name)
self.type_name_with_affixes(message_name)
));
self.depth += 1;

Expand Down Expand Up @@ -579,7 +571,13 @@ impl<'b> CodeGenerator<'_, 'b> {
fq_message_name: &str,
oneof: &OneofField,
) {
let type_name = format!("{}::{}", to_snake(message_name), oneof.type_name());
// Apply OneOf suffix if there's a conflict, then apply global affixes
let mut base_name = oneof.descriptor.name().to_string();
if oneof.has_type_name_conflict {
base_name.push_str("_one_of");
}
let oneof_type_name = self.type_name_with_affixes(&base_name);
let type_name = format!("{}::{}", to_snake(message_name), oneof_type_name);
self.append_doc(fq_message_name, None);
self.push_indent();
self.buf.push_str(&format!(
Expand Down Expand Up @@ -633,7 +631,14 @@ impl<'b> CodeGenerator<'_, 'b> {
self.append_skip_debug(fq_message_name);
self.push_indent();
self.buf.push_str("pub enum ");
self.buf.push_str(&oneof.type_name());

// Apply OneOf suffix if there's a conflict, then apply global affixes
let mut base_name = oneof.descriptor.name().to_string();
if oneof.has_type_name_conflict {
base_name.push_str("_one_of");
}
let oneof_type_name = self.type_name_with_affixes(&base_name);
self.buf.push_str(&oneof_type_name);
self.buf.push_str(" {\n");

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

let proto_enum_name = desc.name();
let enum_name = to_upper_camel(proto_enum_name);
let fq_proto_enum_name = self.fq_name(proto_enum_name);
let enum_name = self.type_name_with_affixes(proto_enum_name);

let enum_values = &desc.value;
let fq_proto_enum_name = self.fq_name(proto_enum_name);

if self
.context
Expand Down Expand Up @@ -995,11 +1000,21 @@ impl<'b> CodeGenerator<'_, 'b> {
ident_path.next();
}

local_path
// Build the base path without the type name
let base_path: Vec<String> = local_path
.map(|_| "super".to_string())
.chain(ident_path.map(to_snake))
.chain(iter::once(to_upper_camel(ident_type)))
.join("::")
.collect();

// Apply prefix/suffix to the type name if configured, using the protobuf identifier to determine package
let type_name = self.type_name_with_affixes_for_package(ident_type, Some(pb_ident));

// Join the path with the potentially suffixed type name
if base_path.is_empty() {
type_name
} else {
format!("{}::{}", base_path.join("::"), type_name)
}
}

fn field_type_tag(&self, field: &FieldDescriptorProto) -> Cow<'static, str> {
Expand Down Expand Up @@ -1069,6 +1084,42 @@ impl<'b> CodeGenerator<'_, 'b> {
message_name,
)
}

/// Return the type name with optional prefix and/or suffix based on configuration
fn type_name_with_affixes(&self, type_name: &str) -> String {
self.type_name_with_affixes_for_package(type_name, None)
}

fn type_name_with_affixes_for_package(
&self,
type_name: &str,
pb_ident: Option<&str>,
) -> String {
let mut type_name = to_upper_camel(type_name);

// Determine the lookup path for PathMap
let lookup_path = match pb_ident {
Some(ident) if ident.starts_with('.') => ident.to_string(),
_ => {
if self.package.is_empty() {
".".to_string()
} else {
format!(".{}", self.package.trim_matches('.'))
}
}
};

// Let PathMap handle the complex path matching and fallback logic
if let Some(prefix) = self.config().type_name_prefixes.get_first(&lookup_path) {
type_name.insert_str(0, prefix);
}

if let Some(suffix) = self.config().type_name_suffixes.get_first(&lookup_path) {
type_name.push_str(suffix);
}

type_name
}
}

/// Returns `true` if the repeated field type can be packed.
Expand Down
134 changes: 134 additions & 0 deletions prost-build/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ pub struct Config {
pub(crate) skip_source_info: bool,
pub(crate) include_file: Option<PathBuf>,
pub(crate) prost_path: Option<String>,
pub(crate) type_name_prefixes: PathMap<String>,
pub(crate) type_name_suffixes: PathMap<String>,
#[cfg(feature = "format")]
pub(crate) fmt: bool,
}
Expand Down Expand Up @@ -353,6 +355,134 @@ impl Config {
self
}

/// Add a suffix to all generated type names.
///
/// # Arguments
///
/// **`suffix`** - an arbitrary string to be appended to all type names. For example,
/// "Proto" would change `MyMessage` to `MyMessageProto`.
///
/// # Examples
///
/// ```rust
/// # let mut config = prost_build::Config::new();
/// // Add "Proto" suffix to all generated types
/// config.type_name_suffix("Proto");
/// ```
pub fn type_name_suffix<S>(&mut self, suffix: S) -> &mut Self
where
S: AsRef<str>,
{
self.type_name_suffixes
.insert(".".to_string(), suffix.as_ref().to_string());
self
}

/// Add a prefix to all generated type names.
///
/// # Arguments
///
/// **`prefix`** - an arbitrary string to be prepended to all type names. For example,
/// "Proto" would change `MyMessage` to `ProtoMyMessage`.
///
/// # Examples
///
/// ```rust
/// # let mut config = prost_build::Config::new();
/// // Add "Proto" prefix to all generated types
/// config.type_name_prefix("Proto");
/// ```
pub fn type_name_prefix<P>(&mut self, prefix: P) -> &mut Self
where
P: AsRef<str>,
{
self.type_name_prefixes
.insert(".".to_string(), prefix.as_ref().to_string());
self
}

/// Configure package-specific type name suffixes.
///
/// This allows different suffix settings for different proto packages,
/// which is especially useful for cross-crate compatibility when importing
/// proto files from external crates.
///
/// # Arguments
///
/// **`paths`** - package paths with their desired suffix. Paths starting with '.'
/// are treated as fully-qualified package names. Paths without a leading '.' are
/// treated as relative and suffix-matched.
///
/// # Examples
///
/// ```rust
/// # let mut config = prost_build::Config::new();
/// // Apply "Proto" suffix to a specific package
/// config.package_type_name_suffix([(".my_package", "Proto")]);
///
/// // Apply different suffixes to different packages
/// config.package_type_name_suffix([
/// (".external_api", "External"), // external API types
/// (".internal", "Internal"), // internal types
/// ]);
///
/// // Apply suffix to all packages under a namespace
/// config.package_type_name_suffix([("my_company", "Pb")]);
/// ```
pub fn package_type_name_suffix<I, P, S>(&mut self, paths: I) -> &mut Self
where
I: IntoIterator<Item = (P, S)>,
P: AsRef<str>,
S: AsRef<str>,
{
for (path, suffix) in paths {
self.type_name_suffixes
.insert(path.as_ref().to_string(), suffix.as_ref().to_string());
}
self
}

/// Configure package-specific type name prefixes.
///
/// This allows different prefix settings for different proto packages,
/// which is especially useful for cross-crate compatibility when importing
/// proto files from external crates.
///
/// # Arguments
///
/// **`paths`** - package paths with their desired prefix. Paths starting with '.'
/// are treated as fully-qualified package names. Paths without a leading '.' are
/// treated as relative and suffix-matched.
///
/// # Examples
///
/// ```rust
/// # let mut config = prost_build::Config::new();
/// // Apply "Proto" prefix to a specific package
/// config.package_type_name_prefix([(".my_package", "Proto")]);
///
/// // Apply different prefixes to different packages
/// config.package_type_name_prefix([
/// (".external_api", "External"), // external API types
/// (".internal", "Internal"), // internal types
/// ]);
///
/// // Apply prefix to all packages under a namespace
/// config.package_type_name_prefix([("my_company", "Pb")]);
/// ```
pub fn package_type_name_prefix<I, P, S>(&mut self, paths: I) -> &mut Self
where
I: IntoIterator<Item = (P, S)>,
P: AsRef<str>,
S: AsRef<str>,
{
for (path, prefix) in paths {
self.type_name_prefixes
.insert(path.as_ref().to_string(), prefix.as_ref().to_string());
}
self
}

/// Wrap matched fields in a `Box`.
///
/// # Arguments
Expand Down Expand Up @@ -1193,6 +1323,8 @@ impl default::Default for Config {
skip_source_info: false,
include_file: None,
prost_path: None,
type_name_prefixes: PathMap::default(),
type_name_suffixes: PathMap::default(),
#[cfg(feature = "format")]
fmt: true,
}
Expand All @@ -1219,6 +1351,8 @@ impl fmt::Debug for Config {
.field("disable_comments", &self.disable_comments)
.field("skip_debug", &self.skip_debug)
.field("prost_path", &self.prost_path)
.field("type_name_prefixes", &self.type_name_prefixes)
.field("type_name_suffixes", &self.type_name_suffixes)
.finish()
}
}
Expand Down
35 changes: 35 additions & 0 deletions tests/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,41 @@ fn main() {
.compile_protos(&[src.join("oneof_name_conflict.proto")], includes)
.unwrap();

// Test type name suffix
prost_build::Config::new()
.type_name_suffix("Proto")
.compile_protos(&[src.join("type_name_suffix.proto")], includes)
.unwrap();

// Test type name prefix
prost_build::Config::new()
.type_name_prefix("Proto")
.compile_protos(&[src.join("type_name_prefix.proto")], includes)
.unwrap();

// Test type name prefix and suffix together
prost_build::Config::new()
.type_name_prefix("Pre")
.type_name_suffix("Post")
.compile_protos(&[src.join("type_name_prefix_suffix.proto")], includes)
.unwrap();

// Test type name suffix with imports (well-known types) and package-specific suffixes
prost_build::Config::new()
.type_name_suffix("Proto")
.package_type_name_suffix([
// Different suffix for external package
(".external_package", "ABC"),
])
.compile_protos(
&[
src.join("type_name_imports.proto"),
src.join("type_name_external_package.proto"),
],
&[src.clone(), src.join("include")],
)
.unwrap();

// Check that attempting to compile a .proto without a package declaration does not result in an error.
config
.compile_protos(&[src.join("no_package.proto")], includes)
Expand Down
12 changes: 12 additions & 0 deletions tests/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,18 @@ mod ident_conversion;
#[cfg(test)]
mod oneof_name_conflict;

#[cfg(test)]
mod type_name_suffix;

#[cfg(test)]
mod type_name_prefix;

#[cfg(test)]
mod type_name_prefix_suffix;

#[cfg(test)]
mod type_name_imports;

mod test_enum_named_option_value {
include!(concat!(env!("OUT_DIR"), "/myenum.optionn.rs"));
}
Expand Down
13 changes: 13 additions & 0 deletions tests/src/type_name_external_package.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
syntax = "proto3";

package external_package;

message ExternalMessage {
string name = 1;
int32 value = 2;
}

enum ExternalStatus {
EXTERNAL_UNKNOWN = 0;
EXTERNAL_ACTIVE = 1;
}
Loading
Loading