diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b157b5dce..eed5c142a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,7 @@ jobs: matrix: toolchain: - stable - - "1.64" + - "1.65" os: - ubuntu-latest - macos-latest diff --git a/prost-build/src/code_generator.rs b/prost-build/src/code_generator.rs index 37771a98b..ccf7e2415 100644 --- a/prost-build/src/code_generator.rs +++ b/prost-build/src/code_generator.rs @@ -84,6 +84,13 @@ impl<'a> CodeGenerator<'a> { code_gen.package ); + if code_gen.config.enable_type_names { + code_gen.buf.push_str(&format!( + "const PACKAGE: &str = \"{}\";\n", + code_gen.package, + )); + } + code_gen.path.push(4); for (idx, message) in file.message_type.into_iter().enumerate() { code_gen.path.push(idx as i32); @@ -261,6 +268,38 @@ 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("const PACKAGE: &'static str = PACKAGE;\n"); + self.buf.push_str(&format!( + "const NAME: &'static str = \"{}\";\n", + message_name + )); + + if let Some(domain_name) = self.config.type_name_domains.get_first(fq_message_name) { + self.buf.push_str(&format!( + r#"fn type_url() -> String {{ + format!("{}/{{}}", Self::full_name()) + }}"#, + domain_name + )); + } + + self.depth -= 1; + self.buf.push_str("}\n"); } fn append_type_attributes(&mut self, fq_message_name: &str) { diff --git a/prost-build/src/lib.rs b/prost-build/src/lib.rs index 67cb13cfc..3fdec73b8 100644 --- a/prost-build/src/lib.rs +++ b/prost-build/src/lib.rs @@ -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. @@ -1257,6 +1299,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 +1326,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/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..68c130a9b --- /dev/null +++ b/tests/src/type_names.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +package type_names; + +message Foo { +} + +message Bar { +} diff --git a/tests/src/type_names.rs b/tests/src/type_names.rs new file mode 100644 index 000000000..2843d115a --- /dev/null +++ b/tests/src/type_names.rs @@ -0,0 +1,15 @@ +use prost::alloc::{format, string::String}; +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!("tests/type_names.Foo", Foo::type_url()); + + assert_eq!("Bar", Bar::NAME); + assert_eq!("type_names", Bar::PACKAGE); + assert_eq!("/type_names.Bar", Bar::type_url()); +}