diff --git a/prost-build/src/code_generator.rs b/prost-build/src/code_generator.rs index 018374528..5544097b0 100644 --- a/prost-build/src/code_generator.rs +++ b/prost-build/src/code_generator.rs @@ -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; @@ -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> { @@ -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; @@ -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; @@ -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!( @@ -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); @@ -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 @@ -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 = 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> { @@ -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. diff --git a/prost-build/src/config.rs b/prost-build/src/config.rs index 192d2e29d..2ffea5abc 100644 --- a/prost-build/src/config.rs +++ b/prost-build/src/config.rs @@ -52,6 +52,8 @@ pub struct Config { pub(crate) skip_source_info: bool, pub(crate) include_file: Option, pub(crate) prost_path: Option, + pub(crate) type_name_prefixes: PathMap, + pub(crate) type_name_suffixes: PathMap, #[cfg(feature = "format")] pub(crate) fmt: bool, } @@ -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(&mut self, suffix: S) -> &mut Self + where + S: AsRef, + { + 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

(&mut self, prefix: P) -> &mut Self + where + P: AsRef, + { + 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(&mut self, paths: I) -> &mut Self + where + I: IntoIterator, + P: AsRef, + S: AsRef, + { + 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(&mut self, paths: I) -> &mut Self + where + I: IntoIterator, + P: AsRef, + S: AsRef, + { + 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 @@ -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, } @@ -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() } } diff --git a/tests/build.rs b/tests/build.rs index 0b15a70aa..39e019b5b 100644 --- a/tests/build.rs +++ b/tests/build.rs @@ -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) diff --git a/tests/src/lib.rs b/tests/src/lib.rs index 8237413bf..193a806be 100644 --- a/tests/src/lib.rs +++ b/tests/src/lib.rs @@ -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")); } diff --git a/tests/src/type_name_external_package.proto b/tests/src/type_name_external_package.proto new file mode 100644 index 000000000..815e95e4b --- /dev/null +++ b/tests/src/type_name_external_package.proto @@ -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; +} diff --git a/tests/src/type_name_imports.proto b/tests/src/type_name_imports.proto new file mode 100644 index 000000000..6f54aeee2 --- /dev/null +++ b/tests/src/type_name_imports.proto @@ -0,0 +1,24 @@ +syntax = "proto3"; + +package type_name_imports; + +import "google/protobuf/timestamp.proto"; +import "type_name_external_package.proto"; + +// Test imported types with package-specific suffixes +message ImportTestMessage { + // Test importing a well-known type (google.protobuf.Timestamp) + // Well-known types are handled by prost_types, so they don't get suffixes + google.protobuf.Timestamp created_at = 1; + + // Test local nested message to ensure suffix applies to local types + message NestedMessage { + string content = 1; + } + + NestedMessage nested = 2; + + // Test importing from external package with different suffix + external_package.ExternalMessage external_ref = 3; + external_package.ExternalStatus external_status = 4; +} diff --git a/tests/src/type_name_imports.rs b/tests/src/type_name_imports.rs new file mode 100644 index 000000000..b0459b925 --- /dev/null +++ b/tests/src/type_name_imports.rs @@ -0,0 +1,37 @@ +// Import test types with suffix applied - tests both well-known types and package-specific suffixes +mod imports_test { + // Include the external package types + pub mod external_package { + include!(concat!(env!("OUT_DIR"), "/external_package.rs")); + } + + // Include the imports test types in a sub-module so super::external_package works + pub mod type_name_imports { + include!(concat!(env!("OUT_DIR"), "/type_name_imports.rs")); + } +} + +use self::imports_test::*; +use alloc::string::ToString; + +#[test] +fn test_well_known_and_package_specific() { + let _ = type_name_imports::ImportTestMessageProto { + // Test well-known type import - google.protobuf.Timestamp uses prost_types::Timestamp + // (Well-known types are handled by prost_types crate, not generated by prost-build, so no suffix) + created_at: Some(::prost_types::Timestamp { + seconds: 1234567890, + nanos: 123456789, + }), + // Test local nested message gets the package suffix ("Proto") + nested: Some(type_name_imports::import_test_message::NestedMessageProto { + content: "test".to_string(), + }), + // Test external package types get their specific suffix ("ABC") + external_ref: Some(external_package::ExternalMessageABC { + name: "external test".to_string(), + value: 42, + }), + external_status: external_package::ExternalStatusABC::ExternalActive as i32, + }; +} diff --git a/tests/src/type_name_prefix.proto b/tests/src/type_name_prefix.proto new file mode 100644 index 000000000..819aa0c1b --- /dev/null +++ b/tests/src/type_name_prefix.proto @@ -0,0 +1,41 @@ +syntax = "proto3"; + +package type_name_prefix; + +// Simple message to test type name prefix +message TestMessage { + string name = 1; + int32 value = 2; +} + +// Enum to test prefix on enums +enum TestEnum { + UNKNOWN = 0; + FIRST = 1; + SECOND = 2; +} + +// Nested types to test prefix propagation +message OuterMessage { + message InnerMessage { + string data = 1; + } + + InnerMessage inner = 1; + TestEnum test_enum = 2; +} + +// Message with self-reference to test prefix in type references +message RecursiveMessage { + RecursiveMessage child = 1; + repeated RecursiveMessage children = 2; +} + +// Message with oneof to test prefix on oneof enum +message MessageWithOneof { + oneof test_oneof { + string string_value = 1; + int32 int_value = 2; + TestMessage message = 3; + } +} diff --git a/tests/src/type_name_prefix.rs b/tests/src/type_name_prefix.rs new file mode 100644 index 000000000..e3b406db7 --- /dev/null +++ b/tests/src/type_name_prefix.rs @@ -0,0 +1,58 @@ +include!(concat!(env!("OUT_DIR"), "/type_name_prefix.rs")); + +use alloc::boxed::Box; +use alloc::string::ToString; +use alloc::vec::Vec; + +#[test] +fn test_type_names_have_prefix() { + // Create instances with the prefixed names + let _ = ProtoTestMessage { + name: "test".to_string(), + value: 42, + }; + + // Test that we can use the enum with prefix + let test_enum = ProtoTestEnum::First; + + // Test nested types have prefix + let _ = ProtoOuterMessage { + inner: Some(outer_message::ProtoInnerMessage { + data: "inner data".to_string(), + }), + test_enum: test_enum as i32, + }; + + // Test self-referencing types work with prefix + let _ = ProtoRecursiveMessage { + child: Some(Box::new(ProtoRecursiveMessage { + child: None, + children: Vec::new(), + })), + children: Vec::from([ProtoRecursiveMessage { + child: None, + children: Vec::new(), + }]), + }; +} + +#[test] +fn test_oneof_with_prefix() { + use self::message_with_oneof::ProtoTestOneof; + + // Test that we can create oneof variants with the prefixed enum name + let _ = ProtoMessageWithOneof { + test_oneof: Some(ProtoTestOneof::StringValue("test".to_string())), + }; + + let _ = ProtoMessageWithOneof { + test_oneof: Some(ProtoTestOneof::IntValue(42)), + }; + + let _ = ProtoMessageWithOneof { + test_oneof: Some(ProtoTestOneof::Message(ProtoTestMessage { + name: "nested".to_string(), + value: 100, + })), + }; +} diff --git a/tests/src/type_name_prefix_suffix.proto b/tests/src/type_name_prefix_suffix.proto new file mode 100644 index 000000000..aaa01bd35 --- /dev/null +++ b/tests/src/type_name_prefix_suffix.proto @@ -0,0 +1,41 @@ +syntax = "proto3"; + +package type_name_prefix_suffix; + +// Simple message to test both prefix and suffix +message TestMessage { + string name = 1; + int32 value = 2; +} + +// Enum to test prefix and suffix on enums +enum TestEnum { + UNKNOWN = 0; + FIRST = 1; + SECOND = 2; +} + +// Nested types to test affix propagation +message OuterMessage { + message InnerMessage { + string data = 1; + } + + InnerMessage inner = 1; + TestEnum test_enum = 2; +} + +// Message with self-reference to test affixes in type references +message RecursiveMessage { + RecursiveMessage child = 1; + repeated RecursiveMessage children = 2; +} + +// Message with oneof to test affixes on oneof enum +message MessageWithOneof { + oneof test_oneof { + string string_value = 1; + int32 int_value = 2; + TestMessage message = 3; + } +} diff --git a/tests/src/type_name_prefix_suffix.rs b/tests/src/type_name_prefix_suffix.rs new file mode 100644 index 000000000..25aa694bd --- /dev/null +++ b/tests/src/type_name_prefix_suffix.rs @@ -0,0 +1,58 @@ +include!(concat!(env!("OUT_DIR"), "/type_name_prefix_suffix.rs")); + +use alloc::boxed::Box; +use alloc::string::ToString; +use alloc::vec::Vec; + +#[test] +fn test_type_names_have_prefix_and_suffix() { + // Create instances with both prefixed and suffixed names + let _ = PreTestMessagePost { + name: "test".to_string(), + value: 42, + }; + + // Test that we can use the enum with prefix and suffix + let test_enum = PreTestEnumPost::First; + + // Test nested types have prefix and suffix + let _ = PreOuterMessagePost { + inner: Some(outer_message::PreInnerMessagePost { + data: "inner data".to_string(), + }), + test_enum: test_enum as i32, + }; + + // Test self-referencing types work with prefix and suffix + let _ = PreRecursiveMessagePost { + child: Some(Box::new(PreRecursiveMessagePost { + child: None, + children: Vec::new(), + })), + children: Vec::from([PreRecursiveMessagePost { + child: None, + children: Vec::new(), + }]), + }; +} + +#[test] +fn test_oneof_with_prefix_and_suffix() { + use self::message_with_oneof::PreTestOneofPost; + + // Test that we can create oneof variants with both prefix and suffix + let _ = PreMessageWithOneofPost { + test_oneof: Some(PreTestOneofPost::StringValue("test".to_string())), + }; + + let _ = PreMessageWithOneofPost { + test_oneof: Some(PreTestOneofPost::IntValue(42)), + }; + + let _ = PreMessageWithOneofPost { + test_oneof: Some(PreTestOneofPost::Message(PreTestMessagePost { + name: "nested".to_string(), + value: 100, + })), + }; +} diff --git a/tests/src/type_name_suffix.proto b/tests/src/type_name_suffix.proto new file mode 100644 index 000000000..62a409730 --- /dev/null +++ b/tests/src/type_name_suffix.proto @@ -0,0 +1,41 @@ +syntax = "proto3"; + +package type_name_suffix; + +// Simple message to test type name suffix +message TestMessage { + string name = 1; + int32 value = 2; +} + +// Enum to test suffix on enums +enum TestEnum { + UNKNOWN = 0; + FIRST = 1; + SECOND = 2; +} + +// Nested types to test suffix propagation +message OuterMessage { + message InnerMessage { + string data = 1; + } + + InnerMessage inner = 1; + TestEnum test_enum = 2; +} + +// Message with self-reference to test suffix in type references +message RecursiveMessage { + RecursiveMessage child = 1; + repeated RecursiveMessage children = 2; +} + +// Message with oneof to test suffix on oneof enum +message MessageWithOneof { + oneof test_oneof { + string string_value = 1; + int32 int_value = 2; + TestMessage message = 3; + } +} diff --git a/tests/src/type_name_suffix.rs b/tests/src/type_name_suffix.rs new file mode 100644 index 000000000..a372202cf --- /dev/null +++ b/tests/src/type_name_suffix.rs @@ -0,0 +1,58 @@ +include!(concat!(env!("OUT_DIR"), "/type_name_suffix.rs")); + +use alloc::boxed::Box; +use alloc::string::ToString; +use alloc::vec::Vec; + +#[test] +fn test_type_names_have_suffix() { + // Create instances with the suffixed names + let _ = TestMessageProto { + name: "test".to_string(), + value: 42, + }; + + // Test that we can use the enum with suffix + let test_enum = TestEnumProto::First; + + // Test nested types have suffix + let _ = OuterMessageProto { + inner: Some(outer_message::InnerMessageProto { + data: "inner data".to_string(), + }), + test_enum: test_enum as i32, + }; + + // Test self-referencing types work with suffix + let _ = RecursiveMessageProto { + child: Some(Box::new(RecursiveMessageProto { + child: None, + children: Vec::new(), + })), + children: Vec::from([RecursiveMessageProto { + child: None, + children: Vec::new(), + }]), + }; +} + +#[test] +fn test_oneof_with_suffix() { + use self::message_with_oneof::TestOneofProto; + + // Test that we can create oneof variants with the suffixed enum name + let _ = MessageWithOneofProto { + test_oneof: Some(TestOneofProto::StringValue("test".to_string())), + }; + + let _ = MessageWithOneofProto { + test_oneof: Some(TestOneofProto::IntValue(42)), + }; + + let _ = MessageWithOneofProto { + test_oneof: Some(TestOneofProto::Message(TestMessageProto { + name: "nested".to_string(), + value: 100, + })), + }; +}