diff --git a/examples/Cargo.toml b/examples/Cargo.toml index aba3531..1e2fddb 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -103,7 +103,7 @@ name = "hyper-warp-server" path = "src/hyper_warp/server.rs" [dependencies] -tonic = { path = "../tonic", features = ["tls"] } +tonic = { path = "../tonic", features = ["tls", "data-prost"] } prost = "0.6" tokio = { version = "0.2", features = ["rt-threaded", "time", "stream", "fs", "macros", "uds"] } futures = { version = "0.3", default-features = false, features = ["alloc"] } @@ -128,4 +128,4 @@ http-body = "0.3" pin-project = "0.4" [build-dependencies] -tonic-build = { path = "../tonic-build" } +tonic-build = { path = "../tonic-build", features = ["prost"] } diff --git a/examples/build.rs b/examples/build.rs index 97ab2d0..f5382a2 100644 --- a/examples/build.rs +++ b/examples/build.rs @@ -1,6 +1,6 @@ fn main() { - tonic_build::compile_protos("proto/helloworld/helloworld.proto").unwrap(); - tonic_build::compile_protos("proto/routeguide/route_guide.proto").unwrap(); - tonic_build::compile_protos("proto/echo/echo.proto").unwrap(); - tonic_build::compile_protos("proto/google/pubsub/pubsub.proto").unwrap(); + tonic_build::prost::compile_protos("proto/helloworld/helloworld.proto").unwrap(); + tonic_build::prost::compile_protos("proto/routeguide/route_guide.proto").unwrap(); + tonic_build::prost::compile_protos("proto/echo/echo.proto").unwrap(); + tonic_build::prost::compile_protos("proto/google/pubsub/pubsub.proto").unwrap(); } diff --git a/examples/helloworld-tutorial.md b/examples/helloworld-tutorial.md index 608768b..ae435b5 100644 --- a/examples/helloworld-tutorial.md +++ b/examples/helloworld-tutorial.md @@ -129,7 +129,7 @@ At the root of your crate, create a `build.rs` file and add the following code: ```rust fn main() -> Result<(), Box> { - tonic_build::compile_protos("proto/helloworld.proto")?; + tonic_build::prost::compile_protos("proto/helloworld.proto")?; Ok(()) } ``` diff --git a/examples/routeguide-tutorial.md b/examples/routeguide-tutorial.md index ffa1bd4..21d4ed5 100644 --- a/examples/routeguide-tutorial.md +++ b/examples/routeguide-tutorial.md @@ -199,7 +199,7 @@ Create a `build.rs` file at the root of your crate: ```rust fn main() { - tonic_build::compile_protos("proto/route_guide.proto") + tonic_build::prost::compile_protos("proto/route_guide.proto") .unwrap_or_else(|e| panic!("Failed to compile protos {:?}", e)); } ``` @@ -823,7 +823,7 @@ opposed to at build time, placing the resulting modules wherever we need them. ```rust fn main() { - tonic_build::configure() + tonic_build::prost::configure() .build_client(false) .out_dir("another_crate/src/pb") .compile(&["path/my_proto.proto"], &["path"]) diff --git a/interop/Cargo.toml b/interop/Cargo.toml index ab1fc2d..053b678 100644 --- a/interop/Cargo.toml +++ b/interop/Cargo.toml @@ -34,4 +34,4 @@ tracing-subscriber = "0.2.0-alpha" tracing-log = "0.1.0" [build-dependencies] -tonic-build = { path = "../tonic-build" } +tonic-build = { path = "../tonic-build", features=["prost"] } diff --git a/interop/build.rs b/interop/build.rs index 7783987..fbc5dcc 100644 --- a/interop/build.rs +++ b/interop/build.rs @@ -1,7 +1,7 @@ fn main() { let proto = "proto/grpc/testing/test.proto"; - tonic_build::compile_protos(proto).unwrap(); + tonic_build::prost::compile_protos(proto).unwrap(); // prevent needing to rebuild if files (or deps) haven't changed println!("cargo:rerun-if-changed={}", proto); diff --git a/tests/extern_path/my_application/build.rs b/tests/extern_path/my_application/build.rs index 5d25b01..893b392 100644 --- a/tests/extern_path/my_application/build.rs +++ b/tests/extern_path/my_application/build.rs @@ -1,5 +1,5 @@ fn main() -> Result<(), std::io::Error> { - tonic_build::configure() + tonic_build::prost::configure() .build_server(false) .build_client(true) .extern_path(".uuid", "::uuid") diff --git a/tests/included_service/build.rs b/tests/included_service/build.rs index 1c29065..b7aeeae 100644 --- a/tests/included_service/build.rs +++ b/tests/included_service/build.rs @@ -1,3 +1,3 @@ fn main() { - tonic_build::compile_protos("proto/includer.proto").unwrap(); + tonic_build::prost::compile_protos("proto/includer.proto").unwrap(); } diff --git a/tests/same_name/build.rs b/tests/same_name/build.rs index ba5ab11..82de4ee 100644 --- a/tests/same_name/build.rs +++ b/tests/same_name/build.rs @@ -1,3 +1,3 @@ fn main() { - tonic_build::compile_protos("proto/foo.proto").unwrap(); + tonic_build::prost::compile_protos("proto/foo.proto").unwrap(); } diff --git a/tests/wellknown/build.rs b/tests/wellknown/build.rs index b3952da..7f0a3c5 100644 --- a/tests/wellknown/build.rs +++ b/tests/wellknown/build.rs @@ -1,3 +1,3 @@ fn main() { - tonic_build::compile_protos("proto/wellknown.proto").unwrap(); + tonic_build::prost::compile_protos("proto/wellknown.proto").unwrap(); } diff --git a/tonic-build/Cargo.toml b/tonic-build/Cargo.toml index d94aec9..83a4632 100644 --- a/tonic-build/Cargo.toml +++ b/tonic-build/Cargo.toml @@ -16,15 +16,16 @@ keywords = ["rpc", "grpc", "async", "codegen", "protobuf"] [dependencies] -prost-build = "0.6" +prost-build = { version = "0.6", optional = true } syn = "1.0" quote = "1.0" proc-macro2 = "1.0" [features] -default = ["transport", "rustfmt"] +default = ["transport", "rustfmt", "prost"] rustfmt = [] transport = [] +prost = ["prost-build"] [package.metadata.docs.rs] all-features = true diff --git a/tonic-build/README.md b/tonic-build/README.md index c7a5652..6a272ae 100644 --- a/tonic-build/README.md +++ b/tonic-build/README.md @@ -23,7 +23,7 @@ tonic-build = ```rust fn main() -> Result<(), Box> { - tonic_build::compile_protos("proto/service.proto")?; + tonic_build::prost::compile_protos("proto/service.proto")?; Ok(()) } ``` @@ -32,7 +32,7 @@ fn main() -> Result<(), Box> { ```rust fn main() -> Result<(), Box> { - tonic_build::configure() + tonic_build::prost::configure() .build_server(false) .compile( &["proto/helloworld/helloworld.proto"], diff --git a/tonic-build/src/client.rs b/tonic-build/src/client.rs index c2b51fe..39b8c04 100644 --- a/tonic-build/src/client.rs +++ b/tonic-build/src/client.rs @@ -1,15 +1,16 @@ +use super::schema::{Context, Method, Service}; use crate::{generate_doc_comments, naive_snake_case}; use proc_macro2::TokenStream; -use prost_build::{Method, Service}; use quote::{format_ident, quote}; -pub(crate) fn generate(service: &Service, proto: &str) -> TokenStream { - let service_ident = quote::format_ident!("{}Client", service.name); - let client_mod = quote::format_ident!("{}_client", naive_snake_case(&service.name)); - let methods = generate_methods(service, proto); +/// Generate service for client +pub fn generate<'a, T: Service<'a>>(service: &'a T, context: &T::Context) -> TokenStream { + let service_ident = quote::format_ident!("{}Client", service.name()); + let client_mod = quote::format_ident!("{}_client", naive_snake_case(&service.name())); + let methods = generate_methods(service, context); let connect = generate_connect(&service_ident); - let service_doc = generate_doc_comments(&service.comments.leading); + let service_doc = generate_doc_comments(service.comment()); quote! { /// Generated client implementations. @@ -75,22 +76,26 @@ fn generate_connect(_service_ident: &syn::Ident) -> TokenStream { TokenStream::new() } -fn generate_methods(service: &Service, proto: &str) -> TokenStream { +fn generate_methods<'a, T: Service<'a>>(service: &'a T, context: &T::Context) -> TokenStream { let mut stream = TokenStream::new(); - for method in &service.methods { + for method in service.methods() { + use super::schema::Commentable; + let path = format!( "/{}.{}/{}", - service.package, service.proto_name, method.proto_name + service.package(), + service.identifier(), + method.identifier() ); - stream.extend(generate_doc_comments(&method.comments.leading)); + stream.extend(generate_doc_comments(method.comment())); - let method = match (method.client_streaming, method.server_streaming) { - (false, false) => generate_unary(method, &proto, path), - (false, true) => generate_server_streaming(method, &proto, path), - (true, false) => generate_client_streaming(method, &proto, path), - (true, true) => generate_streaming(method, &proto, path), + let method = match (method.client_streaming(), method.server_streaming()) { + (false, false) => generate_unary(method, &context, path), + (false, true) => generate_server_streaming(method, &context, path), + (true, false) => generate_client_streaming(method, &context, path), + (true, true) => generate_streaming(method, &context, path), }; stream.extend(method); @@ -99,9 +104,14 @@ fn generate_methods(service: &Service, proto: &str) -> TokenStream { stream } -fn generate_unary(method: &Method, proto: &str, path: String) -> TokenStream { - let ident = format_ident!("{}", method.name); - let (request, response) = crate::replace_wellknown(proto, &method); +fn generate_unary<'a, T: Method<'a>>( + method: &T, + context: &T::Context, + path: String, +) -> TokenStream { + let codec_name = syn::parse_str::(context.codec_name()).unwrap(); + let ident = format_ident!("{}", method.name()); + let (request, response) = method.request_response_name(context); quote! { pub async fn #ident( @@ -111,17 +121,22 @@ fn generate_unary(method: &Method, proto: &str, path: String) -> TokenStream { self.inner.ready().await.map_err(|e| { tonic::Status::new(tonic::Code::Unknown, format!("Service was not ready: {}", e.into())) })?; - let codec = tonic::codec::ProstCodec::default(); + let codec = #codec_name::default(); let path = http::uri::PathAndQuery::from_static(#path); self.inner.unary(request.into_request(), path, codec).await } } } -fn generate_server_streaming(method: &Method, proto: &str, path: String) -> TokenStream { - let ident = format_ident!("{}", method.name); +fn generate_server_streaming<'a, T: Method<'a>>( + method: &T, + context: &T::Context, + path: String, +) -> TokenStream { + let codec_name = syn::parse_str::(context.codec_name()).unwrap(); + let ident = format_ident!("{}", method.name()); - let (request, response) = crate::replace_wellknown(proto, &method); + let (request, response) = method.request_response_name(context); quote! { pub async fn #ident( @@ -131,17 +146,22 @@ fn generate_server_streaming(method: &Method, proto: &str, path: String) -> Toke self.inner.ready().await.map_err(|e| { tonic::Status::new(tonic::Code::Unknown, format!("Service was not ready: {}", e.into())) })?; - let codec = tonic::codec::ProstCodec::default(); + let codec = #codec_name::default(); let path = http::uri::PathAndQuery::from_static(#path); self.inner.server_streaming(request.into_request(), path, codec).await } } } -fn generate_client_streaming(method: &Method, proto: &str, path: String) -> TokenStream { - let ident = format_ident!("{}", method.name); +fn generate_client_streaming<'a, T: Method<'a>>( + method: &T, + context: &T::Context, + path: String, +) -> TokenStream { + let codec_name = syn::parse_str::(context.codec_name()).unwrap(); + let ident = format_ident!("{}", method.name()); - let (request, response) = crate::replace_wellknown(proto, &method); + let (request, response) = method.request_response_name(context); quote! { pub async fn #ident( @@ -151,17 +171,22 @@ fn generate_client_streaming(method: &Method, proto: &str, path: String) -> Toke self.inner.ready().await.map_err(|e| { tonic::Status::new(tonic::Code::Unknown, format!("Service was not ready: {}", e.into())) })?; - let codec = tonic::codec::ProstCodec::default(); + let codec = #codec_name::default(); let path = http::uri::PathAndQuery::from_static(#path); self.inner.client_streaming(request.into_streaming_request(), path, codec).await } } } -fn generate_streaming(method: &Method, proto: &str, path: String) -> TokenStream { - let ident = format_ident!("{}", method.name); +fn generate_streaming<'a, T: Method<'a>>( + method: &T, + context: &T::Context, + path: String, +) -> TokenStream { + let codec_name = syn::parse_str::(context.codec_name()).unwrap(); + let ident = format_ident!("{}", method.name()); - let (request, response) = crate::replace_wellknown(proto, &method); + let (request, response) = method.request_response_name(context); quote! { pub async fn #ident( @@ -171,7 +196,7 @@ fn generate_streaming(method: &Method, proto: &str, path: String) -> TokenStream self.inner.ready().await.map_err(|e| { tonic::Status::new(tonic::Code::Unknown, format!("Service was not ready: {}", e.into())) })?; - let codec = tonic::codec::ProstCodec::default(); + let codec = #codec_name::default(); let path = http::uri::PathAndQuery::from_static(#path); self.inner.streaming(request.into_streaming_request(), path, codec).await } diff --git a/tonic-build/src/lib.rs b/tonic-build/src/lib.rs index 0c75ee2..59b6a6d 100644 --- a/tonic-build/src/lib.rs +++ b/tonic-build/src/lib.rs @@ -23,7 +23,7 @@ //! //! ```rust,no_run //! fn main() -> Result<(), Box> { -//! tonic_build::compile_protos("proto/service.proto")?; +//! tonic_build::prost::compile_protos("proto/service.proto")?; //! Ok(()) //! } //! ``` @@ -32,7 +32,7 @@ //! //! ```rust,no_run //! fn main() -> Result<(), Box> { -//! tonic_build::configure() +//! tonic_build::prost::configure() //! .build_server(false) //! .compile( //! &["proto/helloworld/helloworld.proto"], @@ -57,163 +57,25 @@ #![doc(test(no_crate_inject, attr(deny(rust_2018_idioms))))] use proc_macro2::{Delimiter, Group, Ident, Literal, Punct, Spacing, Span, TokenStream}; -use prost_build::{Config, Method}; -use quote::{ToTokens, TokenStreamExt}; +use quote::TokenStreamExt; + +/// Prost generator +#[cfg(feature = "prost")] +pub mod prost; +/// Traits to describe schema +pub mod schema; #[cfg(feature = "rustfmt")] use std::process::Command; -use std::{ - io, - path::{Path, PathBuf}, -}; -mod client; -mod server; - -/// Service generator builder. -#[derive(Debug, Clone)] -pub struct Builder { - build_client: bool, - build_server: bool, - extern_path: Vec<(String, String)>, - field_attributes: Vec<(String, String)>, - type_attributes: Vec<(String, String)>, - out_dir: Option, - #[cfg(feature = "rustfmt")] - format: bool, -} - -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 the output to be formated by rustfmt. - #[cfg(feature = "rustfmt")] - pub fn format(mut self, run: bool) -> Self { - self.format = run; - 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 - } - - /// Compile the .proto files and execute code generation. - pub fn compile>(self, protos: &[P], includes: &[P]) -> io::Result<()> { - let mut config = Config::new(); - - #[cfg(feature = "rustfmt")] - let format = self.format; - - let out_dir = self - .out_dir - .clone() - .unwrap_or_else(|| PathBuf::from(std::env::var("OUT_DIR").unwrap())); - - config.out_dir(out_dir.clone()); - for (proto_path, rust_path) in self.extern_path.iter() { - config.extern_path(proto_path, rust_path); - } - for (path, attr) in self.field_attributes.iter() { - config.field_attribute(path, attr); - } - for (path, attr) in self.type_attributes.iter() { - config.type_attribute(path, attr); - } - config.service_generator(Box::new(ServiceGenerator::new(self))); - - config.compile_protos(protos, includes)?; - - #[cfg(feature = "rustfmt")] - { - if format { - fmt(out_dir.to_str().expect("Expected utf8 out_dir")); - } - } - - Ok(()) - } -} - -/// 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, - out_dir: None, - extern_path: Vec::new(), - field_attributes: Vec::new(), - type_attributes: Vec::new(), - #[cfg(feature = "rustfmt")] - format: true, - } -} - -/// 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_path: impl AsRef) -> io::Result<()> { - let proto_path: &Path = proto_path.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(&[proto_path], &[proto_dir])?; - - Ok(()) -} +/// Serivce code generation for client +pub mod client; +/// Serivce code generation for Server +pub mod server; +/// Format files under the out_dir with rustfmt #[cfg(feature = "rustfmt")] -fn fmt(out_dir: &str) { +pub fn fmt(out_dir: &str) { let dir = std::fs::read_dir(out_dir).unwrap(); for entry in dir { @@ -232,73 +94,13 @@ fn fmt(out_dir: &str) { } } -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) { - let path = "super"; - - if self.builder.build_server { - let server = server::generate(&service, path); - self.servers.extend(server); - } - - if self.builder.build_client { - let client = client::generate(&service, 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 code = format!("{}", client_service); - 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 code = format!("{}", server_service); - buf.push_str(&code); - - self.servers = TokenStream::default(); - } - } -} - // Generate a singular line of a doc comment -fn generate_doc_comment(comment: &str) -> TokenStream { +fn generate_doc_comment>(comment: S) -> TokenStream { let mut doc_stream = TokenStream::new(); doc_stream.append(Ident::new("doc", Span::call_site())); doc_stream.append(Punct::new('=', Spacing::Alone)); - doc_stream.append(Literal::string(&comment)); + doc_stream.append(Literal::string(comment.as_ref())); let group = Group::new(Delimiter::Bracket, doc_stream); @@ -309,40 +111,18 @@ fn generate_doc_comment(comment: &str) -> TokenStream { } // Generate a larger doc comment composed of many lines of doc comments -fn generate_doc_comments>(comments: &[T]) -> TokenStream { +fn generate_doc_comments<'a, T: AsRef + 'a, C: IntoIterator>( + comments: C, +) -> TokenStream { let mut stream = TokenStream::new(); for comment in comments { - stream.extend(generate_doc_comment(comment.as_ref())); + stream.extend(generate_doc_comment(comment)); } stream } -fn replace_wellknown(proto_path: &str, method: &Method) -> (TokenStream, TokenStream) { - let request = if method.input_proto_type.starts_with(".google.protobuf") - || method.input_type.starts_with("::") - { - method.input_type.parse::().unwrap() - } else { - syn::parse_str::(&format!("{}::{}", proto_path, method.input_type)) - .unwrap() - .to_token_stream() - }; - - let response = if method.output_proto_type.starts_with(".google.protobuf") - || method.output_type.starts_with("::") - { - method.output_type.parse::().unwrap() - } else { - syn::parse_str::(&format!("{}::{}", proto_path, method.output_type)) - .unwrap() - .to_token_stream() - }; - - (request, response) -} - fn naive_snake_case(name: &str) -> String { let mut s = String::new(); let mut it = name.chars().peekable(); diff --git a/tonic-build/src/prost.rs b/tonic-build/src/prost.rs new file mode 100644 index 0000000..924abf0 --- /dev/null +++ b/tonic-build/src/prost.rs @@ -0,0 +1,320 @@ +use super::{client, schema, server}; +use proc_macro2::TokenStream; +use prost_build::{Config, Method, Service}; +use quote::ToTokens; +use std::io; +use std::path::{Path, PathBuf}; + +impl<'a> schema::Commentable<'a> for Service { + type Comment = String; + type CommentContainer = &'a Vec; + + fn comment(&'a self) -> Self::CommentContainer { + &self.comments.leading + } +} + +/// Context data used while generate prost service +#[derive(Debug)] +pub struct ProstContext { + /// relative path to proto definitions from service definitions + pub proto_path: String, +} + +impl schema::Context for ProstContext { + fn codec_name(&self) -> &str { + "tonic::codec::ProstCodec" + } +} + +impl<'a> schema::Service<'a> for Service { + type Method = Method; + type MethodContainer = &'a Vec; + type Context = ProstContext; + + fn name(&self) -> &str { + &self.name + } + + fn package(&self) -> &str { + &self.package + } + + fn identifier(&self) -> &str { + &self.proto_name + } + + fn methods(&'a self) -> Self::MethodContainer { + &self.methods + } +} + +impl<'a> schema::Commentable<'a> for Method { + type Comment = String; + type CommentContainer = &'a Vec; + + fn comment(&'a self) -> Self::CommentContainer { + &self.comments.leading + } +} + +impl<'a> schema::Method<'a> for Method { + type Context = ProstContext; + + fn name(&self) -> &str { + &self.name + } + + fn identifier(&self) -> &str { + &self.proto_name + } + + fn client_streaming(&self) -> bool { + self.client_streaming + } + + fn server_streaming(&self) -> bool { + self.server_streaming + } + + fn request_response_name(&self, context: &Self::Context) -> (TokenStream, TokenStream) { + let request = if self.input_proto_type.starts_with(".google.protobuf") + || self.input_type.starts_with("::") + { + self.input_type.parse::().unwrap() + } else { + syn::parse_str::(&format!("{}::{}", context.proto_path, self.input_type)) + .unwrap() + .to_token_stream() + }; + + let response = if self.output_proto_type.starts_with(".google.protobuf") + || self.output_type.starts_with("::") + { + self.output_type.parse::().unwrap() + } else { + syn::parse_str::(&format!("{}::{}", context.proto_path, self.output_type)) + .unwrap() + .to_token_stream() + }; + + (request, response) + } +} + +pub(crate) fn compile>( + builder: Builder, + out_dir: PathBuf, + protos: &[P], + includes: &[P], +) -> std::io::Result<()> { + let mut config = Config::new(); + + config.out_dir(out_dir); + for (proto_path, rust_path) in builder.extern_path.iter() { + config.extern_path(proto_path, rust_path); + } + for (prost_path, attr) in builder.field_attributes.iter() { + config.field_attribute(prost_path, attr); + } + for (prost_path, attr) in builder.type_attributes.iter() { + config.type_attribute(prost_path, attr); + } + config.service_generator(Box::new(ServiceGenerator::new(builder))); + + config.compile_protos(protos, includes)?; + + Ok(()) +} + +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) { + let context = ProstContext { + proto_path: String::from("super"), + }; + + if self.builder.build_server { + let server = server::generate(&service, &context); + self.servers.extend(server); + } + + if self.builder.build_client { + let client = client::generate(&service, &context); + 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 code = format!("{}", client_service); + 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 code = format!("{}", server_service); + 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) extern_path: Vec<(String, String)>, + pub(crate) field_attributes: Vec<(String, String)>, + pub(crate) type_attributes: Vec<(String, String)>, + + out_dir: Option, + #[cfg(feature = "rustfmt")] + format: bool, +} + +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 the output to be formated by rustfmt. + #[cfg(feature = "rustfmt")] + pub fn format(mut self, run: bool) -> Self { + self.format = run; + 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 + } + + /// Compile the .proto files and execute code generation. + pub fn compile>(self, protos: &[P], includes: &[P]) -> io::Result<()> { + let out_dir = if let Some(out_dir) = self.out_dir.as_ref() { + out_dir.clone() + } else { + PathBuf::from(std::env::var("OUT_DIR").unwrap()) + }; + + #[cfg(feature = "rustfmt")] + let format = self.format; + + compile(self, out_dir.clone(), protos, includes)?; + + #[cfg(feature = "rustfmt")] + { + if format { + super::fmt(out_dir.to_str().expect("Expected utf8 out_dir")); + } + } + + Ok(()) + } +} + +/// 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, + out_dir: None, + extern_path: Vec::new(), + field_attributes: Vec::new(), + type_attributes: Vec::new(), + #[cfg(feature = "rustfmt")] + format: true, + } +} + +/// 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_path: impl AsRef) -> io::Result<()> { + let proto_path: &Path = proto_path.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(&[proto_path], &[proto_dir])?; + + Ok(()) +} diff --git a/tonic-build/src/schema.rs b/tonic-build/src/schema.rs new file mode 100644 index 0000000..7bafa68 --- /dev/null +++ b/tonic-build/src/schema.rs @@ -0,0 +1,53 @@ +use proc_macro2::TokenStream; + +/// Context data used in code generation +pub trait Context { + /// Provide name of tonic compatibale codec + fn codec_name(&self) -> &str; +} + +/// Item has comment +pub trait Commentable<'a> { + /// Comment type + type Comment: AsRef + 'a; + /// Container has comments. + type CommentContainer: IntoIterator; + /// Get comments about this item + fn comment(&'a self) -> Self::CommentContainer; +} + +/// Service +pub trait Service<'a>: Commentable<'a> { + /// Method type + type Method: Method<'a, Context = Self::Context> + 'a; + /// Container has methods + type MethodContainer: IntoIterator; + /// Common context + type Context: Context + 'a; + + /// Name of service + fn name(&self) -> &str; + /// Package name of service + fn package(&self) -> &str; + /// Identifier used to generate type name + fn identifier(&self) -> &str; + /// Methods provided by service + fn methods(&'a self) -> Self::MethodContainer; +} + +/// Method +pub trait Method<'a>: Commentable<'a> { + /// Common context + type Context: Context + 'a; + + /// Name of method + fn name(&self) -> &str; + /// Identifier used to generate type name + fn identifier(&self) -> &str; + /// Method is streamed by client + fn client_streaming(&self) -> bool; + /// Method is streamed by server + fn server_streaming(&self) -> bool; + /// Type name of request and response + fn request_response_name(&self, context: &Self::Context) -> (TokenStream, TokenStream); +} diff --git a/tonic-build/src/server.rs b/tonic-build/src/server.rs index 05bd28a..cf6e237 100644 --- a/tonic-build/src/server.rs +++ b/tonic-build/src/server.rs @@ -1,20 +1,21 @@ +use super::schema::{Commentable, Context, Method, Service}; use crate::{generate_doc_comment, generate_doc_comments, naive_snake_case}; use proc_macro2::{Span, TokenStream}; -use prost_build::{Method, Service}; use quote::quote; use syn::{Ident, Lit, LitStr}; -pub(crate) fn generate(service: &Service, proto_path: &str) -> TokenStream { - let methods = generate_methods(&service, proto_path); +/// Generate service for Server +pub fn generate<'a, T: Service<'a>>(service: &'a T, context: &T::Context) -> TokenStream { + let methods = generate_methods(service, context); - let server_service = quote::format_ident!("{}Server", service.name); - let server_trait = quote::format_ident!("{}", service.name); - let server_mod = quote::format_ident!("{}_server", naive_snake_case(&service.name)); - let generated_trait = generate_trait(service, proto_path, server_trait.clone()); - let service_doc = generate_doc_comments(&service.comments.leading); + let server_service = quote::format_ident!("{}Server", service.name()); + let server_trait = quote::format_ident!("{}", service.name()); + let server_mod = quote::format_ident!("{}_server", naive_snake_case(&service.name())); + let generated_trait = generate_trait(service, context, server_trait.clone()); + let service_doc = generate_doc_comments(service.comment()); // Transport based implementations - let path = format!("{}.{}", service.package, service.proto_name); + let path = format!("{}.{}", service.package(), service.identifier()); let transport = generate_transport(&server_service, &server_trait, &path); quote! { @@ -98,11 +99,15 @@ pub(crate) fn generate(service: &Service, proto_path: &str) -> TokenStream { } } -fn generate_trait(service: &Service, proto_path: &str, server_trait: Ident) -> TokenStream { - let methods = generate_trait_methods(service, proto_path); +fn generate_trait<'a, T: Service<'a>>( + service: &'a T, + context: &T::Context, + server_trait: Ident, +) -> TokenStream { + let methods = generate_trait_methods(service, context); let trait_doc = generate_doc_comment(&format!( "Generated trait containing gRPC methods that should be implemented for use with {}Server.", - service.name + service.name() )); quote! { @@ -114,17 +119,17 @@ fn generate_trait(service: &Service, proto_path: &str, server_trait: Ident) -> T } } -fn generate_trait_methods(service: &Service, proto_path: &str) -> TokenStream { +fn generate_trait_methods<'a, T: Service<'a>>(service: &'a T, context: &T::Context) -> TokenStream { let mut stream = TokenStream::new(); - for method in &service.methods { - let name = quote::format_ident!("{}", method.name); + for method in service.methods() { + let name = quote::format_ident!("{}", method.name()); - let (req_message, res_message) = crate::replace_wellknown(proto_path, &method); + let (req_message, res_message) = method.request_response_name(context); - let method_doc = generate_doc_comments(&method.comments.leading); + let method_doc = generate_doc_comments(method.comment()); - let method = match (method.client_streaming, method.server_streaming) { + let method = match (method.client_streaming(), method.server_streaming()) { (false, false) => { quote! { #method_doc @@ -140,10 +145,10 @@ fn generate_trait_methods(service: &Service, proto_path: &str) -> TokenStream { } } (false, true) => { - let stream = quote::format_ident!("{}Stream", method.proto_name); + let stream = quote::format_ident!("{}Stream", method.identifier()); let stream_doc = generate_doc_comment(&format!( "Server streaming response type for the {} method.", - method.proto_name + method.identifier() )); quote! { @@ -156,10 +161,10 @@ fn generate_trait_methods(service: &Service, proto_path: &str) -> TokenStream { } } (true, true) => { - let stream = quote::format_ident!("{}Stream", method.proto_name); + let stream = quote::format_ident!("{}Stream", method.identifier()); let stream_doc = generate_doc_comment(&format!( "Server streaming response type for the {} method.", - method.proto_name + method.identifier() )); quote! { @@ -203,29 +208,31 @@ fn generate_transport( TokenStream::new() } -fn generate_methods(service: &Service, proto_path: &str) -> TokenStream { +fn generate_methods<'a, T: Service<'a>>(service: &'a T, context: &T::Context) -> TokenStream { let mut stream = TokenStream::new(); - for method in &service.methods { + for method in service.methods() { let path = format!( "/{}.{}/{}", - service.package, service.proto_name, method.proto_name + service.package(), + service.identifier(), + method.identifier() ); let method_path = Lit::Str(LitStr::new(&path, Span::call_site())); - let ident = quote::format_ident!("{}", method.name); - let server_trait = quote::format_ident!("{}", service.name); + let ident = quote::format_ident!("{}", method.name()); + let server_trait = quote::format_ident!("{}", service.name()); - let method_stream = match (method.client_streaming, method.server_streaming) { - (false, false) => generate_unary(method, ident, proto_path, server_trait), + let method_stream = match (method.client_streaming(), method.server_streaming()) { + (false, false) => generate_unary(method, ident, context, server_trait), (false, true) => { - generate_server_streaming(method, ident.clone(), proto_path, server_trait) + generate_server_streaming(method, ident.clone(), context, server_trait) } (true, false) => { - generate_client_streaming(method, ident.clone(), proto_path, server_trait) + generate_client_streaming(method, ident.clone(), context, server_trait) } - (true, true) => generate_streaming(method, ident.clone(), proto_path, server_trait), + (true, true) => generate_streaming(method, ident.clone(), context, server_trait), }; let method = quote! { @@ -239,15 +246,17 @@ fn generate_methods(service: &Service, proto_path: &str) -> TokenStream { stream } -fn generate_unary( - method: &Method, +fn generate_unary<'a, T: Method<'a>>( + method: &T, method_ident: Ident, - proto_path: &str, + context: &T::Context, server_trait: Ident, ) -> TokenStream { - let service_ident = quote::format_ident!("{}Svc", method.proto_name); + let codec_name = syn::parse_str::(context.codec_name()).unwrap(); - let (request, response) = crate::replace_wellknown(proto_path, &method); + let service_ident = quote::format_ident!("{}Svc", method.identifier()); + + let (request, response) = method.request_response_name(context); quote! { struct #service_ident(pub Arc); @@ -270,7 +279,7 @@ fn generate_unary( let interceptor = inner.1.clone(); let inner = inner.0; let method = #service_ident(inner); - let codec = tonic::codec::ProstCodec::default(); + let codec = #codec_name::default(); let mut grpc = if let Some(interceptor) = interceptor { tonic::server::Grpc::with_interceptor(codec, interceptor) @@ -286,17 +295,19 @@ fn generate_unary( } } -fn generate_server_streaming( - method: &Method, +fn generate_server_streaming<'a, T: Method<'a>>( + method: &T, method_ident: Ident, - proto_path: &str, + context: &T::Context, server_trait: Ident, ) -> TokenStream { - let service_ident = quote::format_ident!("{}Svc", method.proto_name); + let codec_name = syn::parse_str::(context.codec_name()).unwrap(); - let (request, response) = crate::replace_wellknown(proto_path, &method); + let service_ident = quote::format_ident!("{}Svc", method.identifier()); - let response_stream = quote::format_ident!("{}Stream", method.proto_name); + let (request, response) = method.request_response_name(context); + + let response_stream = quote::format_ident!("{}Stream", method.identifier()); quote! { struct #service_ident(pub Arc); @@ -321,7 +332,7 @@ fn generate_server_streaming( let interceptor = inner.1; let inner = inner.0; let method = #service_ident(inner); - let codec = tonic::codec::ProstCodec::default(); + let codec = #codec_name::default(); let mut grpc = if let Some(interceptor) = interceptor { tonic::server::Grpc::with_interceptor(codec, interceptor) @@ -337,15 +348,16 @@ fn generate_server_streaming( } } -fn generate_client_streaming( - method: &Method, +fn generate_client_streaming<'a, T: Method<'a>>( + method: &T, method_ident: Ident, - proto_path: &str, + context: &T::Context, server_trait: Ident, ) -> TokenStream { - let service_ident = quote::format_ident!("{}Svc", method.proto_name); + let service_ident = quote::format_ident!("{}Svc", method.identifier()); - let (request, response) = crate::replace_wellknown(proto_path, &method); + let (request, response) = method.request_response_name(context); + let codec_name = syn::parse_str::(context.codec_name()).unwrap(); quote! { struct #service_ident(pub Arc); @@ -370,7 +382,7 @@ fn generate_client_streaming( let interceptor = inner.1; let inner = inner.0; let method = #service_ident(inner); - let codec = tonic::codec::ProstCodec::default(); + let codec = #codec_name::default(); let mut grpc = if let Some(interceptor) = interceptor { tonic::server::Grpc::with_interceptor(codec, interceptor) @@ -386,17 +398,19 @@ fn generate_client_streaming( } } -fn generate_streaming( - method: &Method, +fn generate_streaming<'a, T: Method<'a>>( + method: &T, method_ident: Ident, - proto_path: &str, + context: &T::Context, server_trait: Ident, ) -> TokenStream { - let service_ident = quote::format_ident!("{}Svc", method.proto_name); + let codec_name = syn::parse_str::(context.codec_name()).unwrap(); - let (request, response) = crate::replace_wellknown(proto_path, &method); + let service_ident = quote::format_ident!("{}Svc", method.identifier()); - let response_stream = quote::format_ident!("{}Stream", method.proto_name); + let (request, response) = method.request_response_name(context); + + let response_stream = quote::format_ident!("{}Stream", method.identifier()); quote! { struct #service_ident(pub Arc); @@ -421,7 +435,7 @@ fn generate_streaming( let interceptor = inner.1; let inner = inner.0; let method = #service_ident(inner); - let codec = tonic::codec::ProstCodec::default(); + let codec = #codec_name::default(); let mut grpc = if let Some(interceptor) = interceptor { tonic::server::Grpc::with_interceptor(codec, interceptor) diff --git a/tonic/Cargo.toml b/tonic/Cargo.toml index 10f7261..d5466ae 100644 --- a/tonic/Cargo.toml +++ b/tonic/Cargo.toml @@ -23,8 +23,8 @@ categories = ["web-programming", "network-programming", "asynchronous"] keywords = ["rpc", "grpc", "async", "futures", "protobuf"] [features] -default = ["transport", "codegen"] -codegen = ["async-trait", "prost", "prost-derive"] +default = ["transport", "codegen", "data-prost"] +codegen = ["async-trait"] transport = [ "hyper", "tokio", @@ -35,6 +35,7 @@ transport = [ ] tls = ["transport", "tokio-rustls"] tls-roots = ["tls", "rustls-native-certs"] +data-prost = ["prost", "prost-derive"] # [[bench]] # name = "bench_main" diff --git a/tonic/src/codec/mod.rs b/tonic/src/codec/mod.rs index 1cff217..b6da55d 100644 --- a/tonic/src/codec/mod.rs +++ b/tonic/src/codec/mod.rs @@ -6,18 +6,18 @@ mod buffer; mod decode; mod encode; -#[cfg(feature = "prost")] +#[cfg(feature = "data-prost")] mod prost; -#[cfg(test)] -mod tests; +#[cfg(all(test, feature = "data-prost"))] +mod prost_tests; use std::io; pub use self::decode::Streaming; pub(crate) use self::encode::{encode_client, encode_server}; -#[cfg(feature = "prost")] -#[cfg_attr(docsrs, doc(cfg(feature = "prost")))] +#[cfg(feature = "data-prost")] +#[cfg_attr(docsrs, doc(cfg(feature = "data-prost")))] pub use self::prost::ProstCodec; use crate::Status; pub use buffer::{DecodeBuf, EncodeBuf}; diff --git a/tonic/src/codec/tests.rs b/tonic/src/codec/prost_tests.rs similarity index 100% rename from tonic/src/codec/tests.rs rename to tonic/src/codec/prost_tests.rs