use crate::{code_gen::CodeGenBuilder, compile_settings::CompileSettings}; use super::Attributes; use proc_macro2::TokenStream; use prost_build::{Config, Method, Service}; use quote::ToTokens; use std::{ collections::HashSet, ffi::OsString, io, path::{Path, PathBuf}, }; /// Configure `tonic-build` code generation. /// /// Use [`compile_protos`] instead if you don't need to tweak anything. pub fn configure() -> Builder { Builder { build_client: true, build_server: true, build_transport: true, file_descriptor_set_path: None, skip_protoc_run: false, out_dir: None, extern_path: Vec::new(), field_attributes: Vec::new(), message_attributes: Vec::new(), enum_attributes: Vec::new(), type_attributes: Vec::new(), boxed: Vec::new(), btree_map: None, bytes: None, server_attributes: Attributes::default(), client_attributes: Attributes::default(), proto_path: "super".to_string(), compile_well_known_types: false, emit_package: true, protoc_args: Vec::new(), include_file: None, emit_rerun_if_changed: std::env::var_os("CARGO").is_some(), disable_comments: HashSet::default(), use_arc_self: false, generate_default_stubs: false, compile_settings: CompileSettings::default(), skip_debug: HashSet::default(), } } /// Simple `.proto` compiling. Use [`configure`] instead if you need more options. /// /// The include directory will be the parent folder of the specified path. /// The package name will be the filename without the extension. pub fn compile_protos(proto: impl AsRef) -> io::Result<()> { let proto_path: &Path = proto.as_ref(); // directory the main .proto file resides in let proto_dir = proto_path .parent() .expect("proto file should reside in a directory"); self::configure().compile_protos(&[proto_path], &[proto_dir]) } /// Simple file descriptor set compiling. Use [`configure`] instead if you need more options. pub fn compile_fds(fds: prost_types::FileDescriptorSet) -> io::Result<()> { self::configure().compile_fds(fds) } /// Non-path Rust types allowed for request/response types. const NON_PATH_TYPE_ALLOWLIST: &[&str] = &["()"]; /// Newtype wrapper for prost to add tonic-specific extensions struct TonicBuildService { prost_service: Service, methods: Vec, } impl TonicBuildService { fn new(prost_service: Service, settings: CompileSettings) -> Self { Self { // CompileSettings are currently only consumed method-by-method but if you need them in the Service, here's your spot. // The tonic_build::Service trait specifies that methods are borrowed, so they have to reified up front. methods: prost_service .methods .iter() .map(|prost_method| TonicBuildMethod { prost_method: prost_method.clone(), settings: settings.clone(), }) .collect(), prost_service, } } } /// Newtype wrapper for prost to add tonic-specific extensions struct TonicBuildMethod { prost_method: Method, settings: CompileSettings, } impl crate::Service for TonicBuildService { type Method = TonicBuildMethod; type Comment = String; fn name(&self) -> &str { &self.prost_service.name } fn package(&self) -> &str { &self.prost_service.package } fn identifier(&self) -> &str { &self.prost_service.proto_name } fn comment(&self) -> &[Self::Comment] { &self.prost_service.comments.leading[..] } fn methods(&self) -> &[Self::Method] { &self.methods } } impl crate::Method for TonicBuildMethod { type Comment = String; fn name(&self) -> &str { &self.prost_method.name } fn identifier(&self) -> &str { &self.prost_method.proto_name } /// For code generation, you can override the codec. /// /// You should set the codec path to an import path that has a free /// function like `fn default()`. The default value is tonic::codec::ProstCodec, /// which returns a default-configured ProstCodec. You may wish to configure /// the codec, e.g., with a buffer configuration. /// /// Though ProstCodec implements Default, it is currently only required that /// the function match the Default trait's function spec. fn codec_path(&self) -> &str { &self.settings.codec_path } fn client_streaming(&self) -> bool { self.prost_method.client_streaming } fn server_streaming(&self) -> bool { self.prost_method.server_streaming } fn comment(&self) -> &[Self::Comment] { &self.prost_method.comments.leading[..] } fn deprecated(&self) -> bool { self.prost_method.options.deprecated.unwrap_or_default() } fn request_response_name( &self, proto_path: &str, compile_well_known_types: bool, ) -> (TokenStream, TokenStream) { let convert_type = |proto_type: &str, rust_type: &str| -> TokenStream { if (is_google_type(proto_type) && !compile_well_known_types) || rust_type.starts_with("::") || NON_PATH_TYPE_ALLOWLIST.contains(&rust_type) { rust_type.parse::().unwrap() } else if rust_type.starts_with("crate::") { syn::parse_str::(rust_type) .unwrap() .to_token_stream() } else { syn::parse_str::(&format!("{proto_path}::{rust_type}")) .unwrap() .to_token_stream() } }; let request = convert_type( &self.prost_method.input_proto_type, &self.prost_method.input_type, ); let response = convert_type( &self.prost_method.output_proto_type, &self.prost_method.output_type, ); (request, response) } } fn is_google_type(ty: &str) -> bool { ty.starts_with(".google.protobuf") } struct ServiceGenerator { builder: Builder, clients: TokenStream, servers: TokenStream, } impl ServiceGenerator { fn new(builder: Builder) -> Self { ServiceGenerator { builder, clients: TokenStream::default(), servers: TokenStream::default(), } } } impl prost_build::ServiceGenerator for ServiceGenerator { fn generate(&mut self, service: prost_build::Service, _buf: &mut String) { if self.builder.build_server { let server = CodeGenBuilder::new() .emit_package(self.builder.emit_package) .compile_well_known_types(self.builder.compile_well_known_types) .attributes(self.builder.server_attributes.clone()) .disable_comments(self.builder.disable_comments.clone()) .use_arc_self(self.builder.use_arc_self) .generate_default_stubs(self.builder.generate_default_stubs) .generate_server( &TonicBuildService::new(service.clone(), self.builder.compile_settings.clone()), &self.builder.proto_path, ); self.servers.extend(server); } if self.builder.build_client { let client = CodeGenBuilder::new() .emit_package(self.builder.emit_package) .compile_well_known_types(self.builder.compile_well_known_types) .attributes(self.builder.client_attributes.clone()) .disable_comments(self.builder.disable_comments.clone()) .build_transport(self.builder.build_transport) .generate_client( &TonicBuildService::new(service, self.builder.compile_settings.clone()), &self.builder.proto_path, ); self.clients.extend(client); } } fn finalize(&mut self, buf: &mut String) { if self.builder.build_client && !self.clients.is_empty() { let clients = &self.clients; let client_service = quote::quote! { #clients }; let ast: syn::File = syn::parse2(client_service).expect("not a valid tokenstream"); let code = prettyplease::unparse(&ast); buf.push_str(&code); self.clients = TokenStream::default(); } if self.builder.build_server && !self.servers.is_empty() { let servers = &self.servers; let server_service = quote::quote! { #servers }; let ast: syn::File = syn::parse2(server_service).expect("not a valid tokenstream"); let code = prettyplease::unparse(&ast); buf.push_str(&code); self.servers = TokenStream::default(); } } } /// Service generator builder. #[derive(Debug, Clone)] pub struct Builder { pub(crate) build_client: bool, pub(crate) build_server: bool, pub(crate) build_transport: bool, pub(crate) file_descriptor_set_path: Option, pub(crate) skip_protoc_run: bool, pub(crate) extern_path: Vec<(String, String)>, pub(crate) field_attributes: Vec<(String, String)>, pub(crate) type_attributes: Vec<(String, String)>, pub(crate) message_attributes: Vec<(String, String)>, pub(crate) enum_attributes: Vec<(String, String)>, pub(crate) boxed: Vec, pub(crate) btree_map: Option>, pub(crate) bytes: Option>, pub(crate) server_attributes: Attributes, pub(crate) client_attributes: Attributes, pub(crate) proto_path: String, pub(crate) emit_package: bool, pub(crate) compile_well_known_types: bool, pub(crate) protoc_args: Vec, pub(crate) include_file: Option, pub(crate) emit_rerun_if_changed: bool, pub(crate) disable_comments: HashSet, pub(crate) use_arc_self: bool, pub(crate) generate_default_stubs: bool, pub(crate) compile_settings: CompileSettings, pub(crate) skip_debug: HashSet, out_dir: Option, } impl Builder { /// Enable or disable gRPC client code generation. pub fn build_client(mut self, enable: bool) -> Self { self.build_client = enable; self } /// Enable or disable gRPC server code generation. pub fn build_server(mut self, enable: bool) -> Self { self.build_server = enable; self } /// Enable or disable generated clients and servers to have built-in tonic /// transport features. /// /// When the `transport` feature is disabled this does nothing. pub fn build_transport(mut self, enable: bool) -> Self { self.build_transport = enable; self } /// Generate a file containing the encoded `prost_types::FileDescriptorSet` for protocol buffers /// modules. This is required for implementing gRPC Server Reflection. pub fn file_descriptor_set_path(mut self, path: impl AsRef) -> Self { self.file_descriptor_set_path = Some(path.as_ref().to_path_buf()); self } /// In combination with with file_descriptor_set_path, this can be used to provide a file /// descriptor set as an input file, rather than having prost-build generate the file by /// calling protoc. pub fn skip_protoc_run(mut self) -> Self { self.skip_protoc_run = true; self } /// Set the output directory to generate code to. /// /// Defaults to the `OUT_DIR` environment variable. pub fn out_dir(mut self, out_dir: impl AsRef) -> Self { self.out_dir = Some(out_dir.as_ref().to_path_buf()); self } /// Declare externally provided Protobuf package or type. /// /// Passed directly to `prost_build::Config.extern_path`. /// Note that both the Protobuf path and the rust package paths should both be fully qualified. /// i.e. Protobuf paths should start with "." and rust paths should start with "::" pub fn extern_path(mut self, proto_path: impl AsRef, rust_path: impl AsRef) -> Self { self.extern_path.push(( proto_path.as_ref().to_string(), rust_path.as_ref().to_string(), )); self } /// Add additional attribute to matched messages, enums, and one-offs. /// /// Passed directly to `prost_build::Config.field_attribute`. pub fn field_attribute, A: AsRef>(mut self, path: P, attribute: A) -> Self { self.field_attributes .push((path.as_ref().to_string(), attribute.as_ref().to_string())); self } /// Add additional attribute to matched messages, enums, and one-offs. /// /// Passed directly to `prost_build::Config.type_attribute`. pub fn type_attribute, A: AsRef>(mut self, path: P, attribute: A) -> Self { self.type_attributes .push((path.as_ref().to_string(), attribute.as_ref().to_string())); self } /// Add additional attribute to matched messages. /// /// Passed directly to `prost_build::Config.message_attribute`. pub fn message_attribute, A: AsRef>( mut self, path: P, attribute: A, ) -> Self { self.message_attributes .push((path.as_ref().to_string(), attribute.as_ref().to_string())); self } /// Add additional attribute to matched enums. /// /// Passed directly to `prost_build::Config.enum_attribute`. pub fn enum_attribute, A: AsRef>(mut self, path: P, attribute: A) -> Self { self.enum_attributes .push((path.as_ref().to_string(), attribute.as_ref().to_string())); self } /// Add additional boxed fields. /// /// Passed directly to `prost_build::Config.boxed`. pub fn boxed>(mut self, path: P) -> Self { self.boxed.push(path.as_ref().to_string()); self } /// Configure the code generator to generate Rust `BTreeMap` fields for Protobuf `map` type /// fields. /// /// Passed directly to `prost_build::Config.btree_map`. /// /// Note: previous configured paths for `btree_map` will be cleared. pub fn btree_map(mut self, paths: I) -> Self where I: IntoIterator, S: AsRef, { self.btree_map = Some( paths .into_iter() .map(|path| path.as_ref().to_string()) .collect(), ); self } /// Configure the code generator to generate Rust `bytes::Bytes` fields for Protobuf `bytes` /// type fields. /// /// Passed directly to `prost_build::Config.bytes`. /// /// Note: previous configured paths for `bytes` will be cleared. pub fn bytes(mut self, paths: I) -> Self where I: IntoIterator, S: AsRef, { self.bytes = Some( paths .into_iter() .map(|path| path.as_ref().to_string()) .collect(), ); self } /// Add additional attribute to matched server `mod`s. Matches on the package name. pub fn server_mod_attribute, A: AsRef>( mut self, path: P, attribute: A, ) -> Self { self.server_attributes .push_mod(path.as_ref().to_string(), attribute.as_ref().to_string()); self } /// Add additional attribute to matched service servers. Matches on the service name. pub fn server_attribute, A: AsRef>(mut self, path: P, attribute: A) -> Self { self.server_attributes .push_struct(path.as_ref().to_string(), attribute.as_ref().to_string()); self } /// Add additional attribute to matched client `mod`s. Matches on the package name. pub fn client_mod_attribute, A: AsRef>( mut self, path: P, attribute: A, ) -> Self { self.client_attributes .push_mod(path.as_ref().to_string(), attribute.as_ref().to_string()); self } /// Add additional attribute to matched service clients. Matches on the service name. pub fn client_attribute, A: AsRef>(mut self, path: P, attribute: A) -> Self { self.client_attributes .push_struct(path.as_ref().to_string(), attribute.as_ref().to_string()); self } /// Set the path to where tonic will search for the Request/Response proto structs /// live relative to the module where you call `include_proto!`. /// /// This defaults to `super` since tonic will generate code in a module. pub fn proto_path(mut self, proto_path: impl AsRef) -> Self { self.proto_path = proto_path.as_ref().to_string(); self } /// Configure Prost `protoc_args` build arguments. /// /// Note: Enabling `--experimental_allow_proto3_optional` requires protobuf >= 3.12. pub fn protoc_arg>(mut self, arg: A) -> Self { self.protoc_args.push(arg.as_ref().into()); self } /// Disable service and rpc comments emission. pub fn disable_comments(mut self, path: impl AsRef) -> Self { self.disable_comments.insert(path.as_ref().to_string()); self } /// Emit `Arc` receiver type in server traits instead of `&self`. pub fn use_arc_self(mut self, enable: bool) -> Self { self.use_arc_self = enable; self } /// Emits GRPC endpoints with no attached package. Effectively ignores protofile package declaration from grpc context. /// /// This effectively sets prost's exported package to an empty string. pub fn disable_package_emission(mut self) -> Self { self.emit_package = false; self } /// Enable or disable directing Prost to compile well-known protobuf types instead /// of using the already-compiled versions available in the `prost-types` crate. /// /// This defaults to `false`. pub fn compile_well_known_types(mut self, compile_well_known_types: bool) -> Self { self.compile_well_known_types = compile_well_known_types; self } /// Configures the optional module filename for easy inclusion of all generated Rust files /// /// If set, generates a file (inside the `OUT_DIR` or `out_dir()` as appropriate) which contains /// a set of `pub mod XXX` statements combining to load all Rust files generated. This can allow /// for a shortcut where multiple related proto files have been compiled together resulting in /// a semi-complex set of includes. pub fn include_file(mut self, path: impl AsRef) -> Self { self.include_file = Some(path.as_ref().to_path_buf()); self } /// Enable or disable emitting /// [`cargo:rerun-if-changed=PATH`](https://doc.rust-lang.org/cargo/reference/build-scripts.html#rerun-if-changed) /// instructions for Cargo. /// /// If set, writes instructions to `stdout` for Cargo so that it understands /// when to rerun the build script. By default, this setting is enabled if /// the `CARGO` environment variable is set. The `CARGO` environment /// variable is set by Cargo for build scripts. Therefore, this setting /// should be enabled automatically when run from a build script. However, /// the method of detection is not completely reliable since the `CARGO` /// environment variable can have been set by anything else. If writing the /// instructions to `stdout` is undesirable, you can disable this setting /// explicitly. pub fn emit_rerun_if_changed(mut self, enable: bool) -> Self { self.emit_rerun_if_changed = enable; self } /// Enable or disable directing service generation to providing a default implementation for service methods. /// When this is false all gRPC methods must be explicitly implemented. /// When this is true any unimplemented service methods will return 'unimplemented' gRPC error code. /// When this is true all streaming server request RPC types explicitly use tonic::codegen::BoxStream type. /// /// This defaults to `false`. pub fn generate_default_stubs(mut self, enable: bool) -> Self { self.generate_default_stubs = enable; self } /// Override the default codec. /// /// If set, writes `{codec_path}::default()` in generated code wherever a codec is created. /// /// This defaults to `"tonic::codec::ProstCodec"` pub fn codec_path(mut self, codec_path: impl Into) -> Self { self.compile_settings.codec_path = codec_path.into(); self } /// Skips generating `impl Debug` for types pub fn skip_debug(mut self, path: impl AsRef) -> Self { self.skip_debug.insert(path.as_ref().to_string()); self } /// Compile the .proto files and execute code generation. pub fn compile_protos( self, protos: &[impl AsRef], includes: &[impl AsRef], ) -> io::Result<()> { self.compile_protos_with_config(Config::new(), protos, includes) } /// Compile the .proto files and execute code generation using a custom /// `prost_build::Config`. The provided config will be updated with this builder's config. pub fn compile_protos_with_config( self, mut config: Config, protos: &[impl AsRef], includes: &[impl AsRef], ) -> io::Result<()> { if self.emit_rerun_if_changed { for path in protos.iter() { println!("cargo:rerun-if-changed={}", path.as_ref().display()) } for path in includes.iter() { // Cargo will watch the **entire** directory recursively. If we // could figure out which files are imported by our protos we // could specify only those files instead. println!("cargo:rerun-if-changed={}", path.as_ref().display()) } } self.setup_config(&mut config); config.compile_protos(protos, includes) } /// Execute code generation from a file descriptor set. pub fn compile_fds(self, fds: prost_types::FileDescriptorSet) -> io::Result<()> { self.compile_fds_with_config(Config::new(), fds) } /// Execute code generation from a file descriptor set using a custom `prost_build::Config`. pub fn compile_fds_with_config( self, mut config: Config, fds: prost_types::FileDescriptorSet, ) -> io::Result<()> { self.setup_config(&mut config); config.compile_fds(fds) } fn setup_config(self, config: &mut Config) { if let Some(out_dir) = self.out_dir.as_ref() { config.out_dir(out_dir); } if let Some(path) = self.file_descriptor_set_path.as_ref() { config.file_descriptor_set_path(path); } if self.skip_protoc_run { config.skip_protoc_run(); } for (proto_path, rust_path) in self.extern_path.iter() { config.extern_path(proto_path, rust_path); } for (prost_path, attr) in self.field_attributes.iter() { config.field_attribute(prost_path, attr); } for (prost_path, attr) in self.type_attributes.iter() { config.type_attribute(prost_path, attr); } for (prost_path, attr) in self.message_attributes.iter() { config.message_attribute(prost_path, attr); } for (prost_path, attr) in self.enum_attributes.iter() { config.enum_attribute(prost_path, attr); } for prost_path in self.boxed.iter() { config.boxed(prost_path); } if let Some(ref paths) = self.btree_map { config.btree_map(paths); } if let Some(ref paths) = self.bytes { config.bytes(paths); } if self.compile_well_known_types { config.compile_well_known_types(); } if let Some(path) = self.include_file.as_ref() { config.include_file(path); } if !self.skip_debug.is_empty() { config.skip_debug(&self.skip_debug); } for arg in self.protoc_args.iter() { config.protoc_arg(arg); } config.service_generator(self.service_generator()); } /// Turn the builder into a `ServiceGenerator` ready to be passed to `prost-build`s /// `Config::service_generator`. pub fn service_generator(self) -> Box { Box::new(ServiceGenerator::new(self)) } }