diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 53b8fca68..baae3c007 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,7 @@ jobs: matrix: toolchain: - stable - - "1.64" + - "1.70" os: - ubuntu-latest - macos-latest diff --git a/Cargo.toml b/Cargo.toml index b647db0cb..893faf3d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "prost" -version = "0.12.1" +version = "0.12.3" authors = [ "Dan Burkert ", "Lucio Franco ", "Lucio Franco ", @@ -12,7 +12,7 @@ documentation = "https://docs.rs/prost-build" readme = "README.md" description = "A Protocol Buffers implementation for the Rust Language." edition = "2021" -rust-version = "1.60" +rust-version = "1.70" [features] default = ["format"] @@ -27,8 +27,8 @@ itertools = { version = ">=0.10, <0.12", default-features = false, features = [" log = "0.4.4" multimap = { version = "0.8", default-features = false } petgraph = { version = "0.6", default-features = false } -prost = { version = "0.12.1", path = "..", default-features = false } -prost-types = { version = "0.12.1", path = "../prost-types", default-features = false } +prost = { version = "0.12.3", path = "..", default-features = false } +prost-types = { version = "0.12.3", path = "../prost-types", default-features = false } tempfile = "3" once_cell = "1.17.1" regex = { version = "1.8.1", default-features = false, features = ["std", "unicode-bool"] } diff --git a/prost-build/src/code_generator.rs b/prost-build/src/code_generator.rs index 37771a98b..4f348baaf 100644 --- a/prost-build/src/code_generator.rs +++ b/prost-build/src/code_generator.rs @@ -29,6 +29,7 @@ enum Syntax { pub struct CodeGenerator<'a> { config: &'a mut Config, package: String, + type_path: Vec, source_info: Option, syntax: Syntax, message_graph: &'a MessageGraph, @@ -69,6 +70,7 @@ impl<'a> CodeGenerator<'a> { let mut code_gen = CodeGenerator { config, package: file.package.unwrap_or_default(), + type_path: Vec::new(), source_info, syntax, message_graph, @@ -121,10 +123,16 @@ impl<'a> CodeGenerator<'a> { let message_name = message.name().to_string(); let fq_message_name = format!( - "{}{}.{}", - if self.package.is_empty() { "" } else { "." }, - self.package, - message.name() + "{}{}{}{}.{}", + if self.package.is_empty() && self.type_path.is_empty() { + "" + } else { + "." + }, + self.package.trim_matches('.'), + if self.type_path.is_empty() { "" } else { "." }, + self.type_path.join("."), + message_name, ); // Skip external types. @@ -261,6 +269,55 @@ impl<'a> CodeGenerator<'a> { self.pop_mod(); } + + if self.config.enable_type_names { + self.append_type_name(&message_name, &fq_message_name); + } + } + + fn append_type_name(&mut self, message_name: &str, fq_message_name: &str) { + self.buf.push_str(&format!( + "impl {}::Name for {} {{\n", + self.config.prost_path.as_deref().unwrap_or("::prost"), + to_upper_camel(&message_name) + )); + self.depth += 1; + + self.buf.push_str(&format!( + "const NAME: &'static str = \"{}\";\n", + message_name, + )); + self.buf.push_str(&format!( + "const PACKAGE: &'static str = \"{}\";\n", + self.package, + )); + + let prost_path = self.config.prost_path.as_deref().unwrap_or("::prost"); + let string_path = format!("{prost_path}::alloc::string::String"); + + let full_name = format!( + "{}{}{}{}{message_name}", + self.package.trim_matches('.'), + if self.package.is_empty() { "" } else { "." }, + self.type_path.join("."), + if self.type_path.is_empty() { "" } else { "." }, + ); + let domain_name = self + .config + .type_name_domains + .get_first(fq_message_name) + .map_or("", |name| name.as_str()); + + self.buf.push_str(&format!( + r#"fn full_name() -> {string_path} {{ "{full_name}".into() }}"#, + )); + + self.buf.push_str(&format!( + r#"fn type_url() -> {string_path} {{ "{domain_name}/{full_name}".into() }}"#, + )); + + self.depth -= 1; + self.buf.push_str("}\n"); } fn append_type_attributes(&mut self, fq_message_name: &str) { @@ -645,11 +702,18 @@ impl<'a> CodeGenerator<'a> { let enum_values = &desc.value; let fq_proto_enum_name = format!( - "{}{}.{}", - if self.package.is_empty() { "" } else { "." }, - self.package, - proto_enum_name + "{}{}{}{}.{}", + if self.package.is_empty() && self.type_path.is_empty() { + "" + } else { + "." + }, + self.package.trim_matches('.'), + if self.type_path.is_empty() { "" } else { "." }, + self.type_path.join("."), + proto_enum_name, ); + if self .extern_paths .resolve_ident(&fq_proto_enum_name) @@ -867,8 +931,7 @@ impl<'a> CodeGenerator<'a> { self.buf.push_str(&to_snake(module)); self.buf.push_str(" {\n"); - self.package.push('.'); - self.package.push_str(module); + self.type_path.push(module.into()); self.depth += 1; } @@ -876,8 +939,7 @@ impl<'a> CodeGenerator<'a> { fn pop_mod(&mut self) { self.depth -= 1; - let idx = self.package.rfind('.').unwrap(); - self.package.truncate(idx); + self.type_path.pop(); self.push_indent(); self.buf.push_str("}\n"); @@ -915,7 +977,11 @@ impl<'a> CodeGenerator<'a> { return proto_ident; } - let mut local_path = self.package.split('.').peekable(); + let mut local_path = self + .package + .split('.') + .chain(self.type_path.iter().map(String::as_str)) + .peekable(); // If no package is specified the start of the package name will be '.' // and split will return an empty string ("") which breaks resolution diff --git a/prost-build/src/fixtures/alphabet/_expected_include.rs b/prost-build/src/fixtures/alphabet/_expected_include.rs index 8f42b561e..87de4feed 100644 --- a/prost-build/src/fixtures/alphabet/_expected_include.rs +++ b/prost-build/src/fixtures/alphabet/_expected_include.rs @@ -1,3 +1,4 @@ +// This file is @generated by prost-build. pub mod a { include!("a.rs"); } diff --git a/prost-build/src/fixtures/field_attributes/_expected_field_attributes.rs b/prost-build/src/fixtures/field_attributes/_expected_field_attributes.rs index 04860e63d..95fb05d86 100644 --- a/prost-build/src/fixtures/field_attributes/_expected_field_attributes.rs +++ b/prost-build/src/fixtures/field_attributes/_expected_field_attributes.rs @@ -1,3 +1,4 @@ +// This file is @generated by prost-build. #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct Container { diff --git a/prost-build/src/fixtures/field_attributes/_expected_field_attributes_formatted.rs b/prost-build/src/fixtures/field_attributes/_expected_field_attributes_formatted.rs index 8c329f902..f1eaee751 100644 --- a/prost-build/src/fixtures/field_attributes/_expected_field_attributes_formatted.rs +++ b/prost-build/src/fixtures/field_attributes/_expected_field_attributes_formatted.rs @@ -1,3 +1,4 @@ +// This file is @generated by prost-build. #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct Container { diff --git a/prost-build/src/fixtures/helloworld/_expected_helloworld.rs b/prost-build/src/fixtures/helloworld/_expected_helloworld.rs index 2f05c46ed..401ee90cd 100644 --- a/prost-build/src/fixtures/helloworld/_expected_helloworld.rs +++ b/prost-build/src/fixtures/helloworld/_expected_helloworld.rs @@ -1,3 +1,4 @@ +// This file is @generated by prost-build. #[derive(derive_builder::Builder)] #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] diff --git a/prost-build/src/fixtures/helloworld/_expected_helloworld_formatted.rs b/prost-build/src/fixtures/helloworld/_expected_helloworld_formatted.rs index a64c4da3c..3f688c7e0 100644 --- a/prost-build/src/fixtures/helloworld/_expected_helloworld_formatted.rs +++ b/prost-build/src/fixtures/helloworld/_expected_helloworld_formatted.rs @@ -1,3 +1,4 @@ +// This file is @generated by prost-build. #[derive(derive_builder::Builder)] #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] diff --git a/prost-build/src/fixtures/imports_empty/_expected_include.rs b/prost-build/src/fixtures/imports_empty/_expected_include.rs index 6deb45f07..a9dcc9386 100644 --- a/prost-build/src/fixtures/imports_empty/_expected_include.rs +++ b/prost-build/src/fixtures/imports_empty/_expected_include.rs @@ -1,3 +1,4 @@ +// This file is @generated by prost-build. pub mod com { pub mod prost_test { pub mod test { diff --git a/prost-build/src/lib.rs b/prost-build/src/lib.rs index 67cb13cfc..9b71094b3 100644 --- a/prost-build/src/lib.rs +++ b/prost-build/src/lib.rs @@ -1,4 +1,4 @@ -#![doc(html_root_url = "https://docs.rs/prost-build/0.12.1")] +#![doc(html_root_url = "https://docs.rs/prost-build/0.12.2")] #![allow(clippy::option_as_ref_deref, clippy::format_push_string)] //! `prost-build` compiles `.proto` files into Rust. @@ -253,6 +253,8 @@ pub struct Config { out_dir: Option, extern_paths: Vec<(String, String)>, default_package_filename: String, + enable_type_names: bool, + type_name_domains: PathMap, protoc_args: Vec, disable_comments: PathMap<()>, skip_debug: PathMap<()>, @@ -839,6 +841,46 @@ impl Config { self } + /// Configures the code generator to include type names. + /// + /// Message types will implement `Name` trait, which provides type and package name. + /// This is needed for encoding messages as `Any` type. + pub fn enable_type_names(&mut self) -> &mut Self { + self.enable_type_names = true; + self + } + + /// Specify domain names to use with message type URLs. + /// + /// # Domains + /// + /// **`paths`** - a path matching any number of types. It works the same way as in + /// [`btree_map`](#method.btree_map), just with the field name omitted. + /// + /// **`domain`** - an arbitrary string to be used as a prefix for type URLs. + /// + /// # Examples + /// + /// ```rust + /// # let mut config = prost_build::Config::new(); + /// // Full type URL of the message `google.profile.Person`, + /// // will be `type.googleapis.com/google.profile.Person`. + /// config.type_name_domain(&["."], "type.googleapis.com"); + /// ``` + pub fn type_name_domain(&mut self, paths: I, domain: D) -> &mut Self + where + I: IntoIterator, + S: AsRef, + D: AsRef, + { + self.type_name_domains.clear(); + for matcher in paths { + self.type_name_domains + .insert(matcher.as_ref().to_string(), domain.as_ref().to_string()); + } + self + } + /// Configures the path that's used for deriving `Message` for generated messages. /// This is mainly useful for generating crates that wish to re-export prost. /// Defaults to `::prost::Message` if not specified. @@ -989,6 +1031,7 @@ impl Config { if let Some(ref include_file) = self.include_file { trace!("Writing include file: {:?}", target.join(include_file)); let mut file = fs::File::create(target.join(include_file))?; + self.write_line(&mut file, 0, "// This file is @generated by prost-build.")?; self.write_includes( modules.keys().collect(), &mut file, @@ -1224,9 +1267,18 @@ impl Config { self.fmt_modules(&mut modules); } + self.add_generated_modules(&mut modules); + Ok(modules) } + fn add_generated_modules(&mut self, modules: &mut HashMap) { + for buf in modules.values_mut() { + let with_generated = "// This file is @generated by prost-build.\n".to_string() + buf; + *buf = with_generated; + } + } + #[cfg(feature = "format")] fn fmt_modules(&mut self, modules: &mut HashMap) { for buf in modules.values_mut() { @@ -1257,6 +1309,8 @@ impl default::Default for Config { out_dir: None, extern_paths: Vec::new(), default_package_filename: "_".to_string(), + enable_type_names: false, + type_name_domains: PathMap::default(), protoc_args: Vec::new(), disable_comments: PathMap::default(), skip_debug: PathMap::default(), @@ -1282,6 +1336,8 @@ impl fmt::Debug for Config { .field("out_dir", &self.out_dir) .field("extern_paths", &self.extern_paths) .field("default_package_filename", &self.default_package_filename) + .field("enable_type_names", &self.enable_type_names) + .field("type_name_domains", &self.type_name_domains) .field("protoc_args", &self.protoc_args) .field("disable_comments", &self.disable_comments) .field("skip_debug", &self.skip_debug) diff --git a/prost-derive/Cargo.toml b/prost-derive/Cargo.toml index 69f5df049..daaaf113a 100644 --- a/prost-derive/Cargo.toml +++ b/prost-derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "prost-derive" -version = "0.12.1" +version = "0.12.3" authors = [ "Dan Burkert ", "Lucio Franco ", @@ -12,7 +12,7 @@ documentation = "https://docs.rs/prost-derive" readme = "README.md" description = "A Protocol Buffers implementation for the Rust Language." edition = "2021" -rust-version = "1.60" +rust-version = "1.70" [lib] proc_macro = true diff --git a/prost-derive/src/lib.rs b/prost-derive/src/lib.rs index 8bc99c5ed..9782b860e 100644 --- a/prost-derive/src/lib.rs +++ b/prost-derive/src/lib.rs @@ -1,4 +1,4 @@ -#![doc(html_root_url = "https://docs.rs/prost-derive/0.12.1")] +#![doc(html_root_url = "https://docs.rs/prost-derive/0.12.2")] // The `quote!` macro requires deep recursion. #![recursion_limit = "4096"] diff --git a/prost-types/Cargo.toml b/prost-types/Cargo.toml index dd610726e..34b8ddf03 100644 --- a/prost-types/Cargo.toml +++ b/prost-types/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "prost-types" -version = "0.12.1" +version = "0.12.3" authors = [ "Dan Burkert ", "Lucio Franco String { - format!("{}.{}", Self::NAME, Self::PACKAGE) + format!("{}.{}", Self::PACKAGE, Self::NAME) } - /// Type URL for this message, which by default is the full name with a + /// Type URL for this [`Message`], which by default is the full name with a /// leading slash, but may also include a leading domain name, e.g. /// `type.googleapis.com/google.profile.Person`. + /// This can be used when serializing with the [`Any`] type. fn type_url() -> String { format!("/{}", Self::full_name()) } diff --git a/tests/single-include/src/outdir/mod.rs b/tests/single-include/src/outdir/mod.rs index 8cf70ccb6..400d90465 100644 --- a/tests/single-include/src/outdir/mod.rs +++ b/tests/single-include/src/outdir/mod.rs @@ -1,3 +1,4 @@ +// This file is @generated by prost-build. pub mod outdir { include!("outdir.rs"); } diff --git a/tests/single-include/src/outdir/outdir.rs b/tests/single-include/src/outdir/outdir.rs index eee461c1b..577245e5f 100644 --- a/tests/single-include/src/outdir/outdir.rs +++ b/tests/single-include/src/outdir/outdir.rs @@ -1,3 +1,4 @@ +// This file is @generated by prost-build. #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct OutdirRequest { diff --git a/tests/src/build.rs b/tests/src/build.rs index bfb086a68..aec3f6458 100644 --- a/tests/src/build.rs +++ b/tests/src/build.rs @@ -149,6 +149,12 @@ fn main() { ) .unwrap(); + prost_build::Config::new() + .enable_type_names() + .type_name_domain(&[".type_names.Foo"], "tests") + .compile_protos(&[src.join("type_names.proto")], includes) + .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 c03c94c10..954c6b367 100644 --- a/tests/src/lib.rs +++ b/tests/src/lib.rs @@ -45,6 +45,8 @@ mod no_unused_results; #[cfg(test)] #[cfg(feature = "std")] mod skip_debug; +#[cfg(test)] +mod type_names; mod test_enum_named_option_value { include!(concat!(env!("OUT_DIR"), "/myenum.optionn.rs")); diff --git a/tests/src/type_names.proto b/tests/src/type_names.proto new file mode 100644 index 000000000..58c599d17 --- /dev/null +++ b/tests/src/type_names.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +package type_names; + +message Foo { + message Bar { + } +} + +message Baz { +} \ No newline at end of file diff --git a/tests/src/type_names.rs b/tests/src/type_names.rs new file mode 100644 index 000000000..09fb41aa1 --- /dev/null +++ b/tests/src/type_names.rs @@ -0,0 +1,21 @@ +use prost::Name; + +include!(concat!(env!("OUT_DIR"), "/type_names.rs")); + +#[test] +fn valid_type_names() { + assert_eq!("Foo", Foo::NAME); + assert_eq!("type_names", Foo::PACKAGE); + assert_eq!("type_names.Foo", Foo::full_name()); + assert_eq!("tests/type_names.Foo", Foo::type_url()); + + assert_eq!("Bar", foo::Bar::NAME); + assert_eq!("type_names", foo::Bar::PACKAGE); + assert_eq!("type_names.Foo.Bar", foo::Bar::full_name()); + assert_eq!("tests/type_names.Foo.Bar", foo::Bar::type_url()); + + assert_eq!("Baz", Baz::NAME); + assert_eq!("type_names", Baz::PACKAGE); + assert_eq!("type_names.Baz", Baz::full_name()); + assert_eq!("/type_names.Baz", Baz::type_url()); +}