feat(build): Decouple codgen from prost (#170)

This commit is contained in:
Gyusun Yeom
2020-03-01 13:52:31 -05:00
committed by GitHub
parent 1b3d107206
commit f65cda1ea0
20 changed files with 550 additions and 356 deletions
+2 -2
View File
@@ -103,7 +103,7 @@ name = "hyper-warp-server"
path = "src/hyper_warp/server.rs" path = "src/hyper_warp/server.rs"
[dependencies] [dependencies]
tonic = { path = "../tonic", features = ["tls"] } tonic = { path = "../tonic", features = ["tls", "data-prost"] }
prost = "0.6" prost = "0.6"
tokio = { version = "0.2", features = ["rt-threaded", "time", "stream", "fs", "macros", "uds"] } tokio = { version = "0.2", features = ["rt-threaded", "time", "stream", "fs", "macros", "uds"] }
futures = { version = "0.3", default-features = false, features = ["alloc"] } futures = { version = "0.3", default-features = false, features = ["alloc"] }
@@ -128,4 +128,4 @@ http-body = "0.3"
pin-project = "0.4" pin-project = "0.4"
[build-dependencies] [build-dependencies]
tonic-build = { path = "../tonic-build" } tonic-build = { path = "../tonic-build", features = ["prost"] }
+4 -4
View File
@@ -1,6 +1,6 @@
fn main() { fn main() {
tonic_build::compile_protos("proto/helloworld/helloworld.proto").unwrap(); tonic_build::prost::compile_protos("proto/helloworld/helloworld.proto").unwrap();
tonic_build::compile_protos("proto/routeguide/route_guide.proto").unwrap(); tonic_build::prost::compile_protos("proto/routeguide/route_guide.proto").unwrap();
tonic_build::compile_protos("proto/echo/echo.proto").unwrap(); tonic_build::prost::compile_protos("proto/echo/echo.proto").unwrap();
tonic_build::compile_protos("proto/google/pubsub/pubsub.proto").unwrap(); tonic_build::prost::compile_protos("proto/google/pubsub/pubsub.proto").unwrap();
} }
+1 -1
View File
@@ -129,7 +129,7 @@ At the root of your crate, create a `build.rs` file and add the following code:
```rust ```rust
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
tonic_build::compile_protos("proto/helloworld.proto")?; tonic_build::prost::compile_protos("proto/helloworld.proto")?;
Ok(()) Ok(())
} }
``` ```
+2 -2
View File
@@ -199,7 +199,7 @@ Create a `build.rs` file at the root of your crate:
```rust ```rust
fn main() { 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)); .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 ```rust
fn main() { fn main() {
tonic_build::configure() tonic_build::prost::configure()
.build_client(false) .build_client(false)
.out_dir("another_crate/src/pb") .out_dir("another_crate/src/pb")
.compile(&["path/my_proto.proto"], &["path"]) .compile(&["path/my_proto.proto"], &["path"])
+1 -1
View File
@@ -34,4 +34,4 @@ tracing-subscriber = "0.2.0-alpha"
tracing-log = "0.1.0" tracing-log = "0.1.0"
[build-dependencies] [build-dependencies]
tonic-build = { path = "../tonic-build" } tonic-build = { path = "../tonic-build", features=["prost"] }
+1 -1
View File
@@ -1,7 +1,7 @@
fn main() { fn main() {
let proto = "proto/grpc/testing/test.proto"; 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 // prevent needing to rebuild if files (or deps) haven't changed
println!("cargo:rerun-if-changed={}", proto); println!("cargo:rerun-if-changed={}", proto);
+1 -1
View File
@@ -1,5 +1,5 @@
fn main() -> Result<(), std::io::Error> { fn main() -> Result<(), std::io::Error> {
tonic_build::configure() tonic_build::prost::configure()
.build_server(false) .build_server(false)
.build_client(true) .build_client(true)
.extern_path(".uuid", "::uuid") .extern_path(".uuid", "::uuid")
+1 -1
View File
@@ -1,3 +1,3 @@
fn main() { fn main() {
tonic_build::compile_protos("proto/includer.proto").unwrap(); tonic_build::prost::compile_protos("proto/includer.proto").unwrap();
} }
+1 -1
View File
@@ -1,3 +1,3 @@
fn main() { fn main() {
tonic_build::compile_protos("proto/foo.proto").unwrap(); tonic_build::prost::compile_protos("proto/foo.proto").unwrap();
} }
+1 -1
View File
@@ -1,3 +1,3 @@
fn main() { fn main() {
tonic_build::compile_protos("proto/wellknown.proto").unwrap(); tonic_build::prost::compile_protos("proto/wellknown.proto").unwrap();
} }
+3 -2
View File
@@ -16,15 +16,16 @@ keywords = ["rpc", "grpc", "async", "codegen", "protobuf"]
[dependencies] [dependencies]
prost-build = "0.6" prost-build = { version = "0.6", optional = true }
syn = "1.0" syn = "1.0"
quote = "1.0" quote = "1.0"
proc-macro2 = "1.0" proc-macro2 = "1.0"
[features] [features]
default = ["transport", "rustfmt"] default = ["transport", "rustfmt", "prost"]
rustfmt = [] rustfmt = []
transport = [] transport = []
prost = ["prost-build"]
[package.metadata.docs.rs] [package.metadata.docs.rs]
all-features = true all-features = true
+2 -2
View File
@@ -23,7 +23,7 @@ tonic-build = <tonic-version>
```rust ```rust
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
tonic_build::compile_protos("proto/service.proto")?; tonic_build::prost::compile_protos("proto/service.proto")?;
Ok(()) Ok(())
} }
``` ```
@@ -32,7 +32,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
```rust ```rust
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
tonic_build::configure() tonic_build::prost::configure()
.build_server(false) .build_server(false)
.compile( .compile(
&["proto/helloworld/helloworld.proto"], &["proto/helloworld/helloworld.proto"],
+56 -31
View File
@@ -1,15 +1,16 @@
use super::schema::{Context, Method, Service};
use crate::{generate_doc_comments, naive_snake_case}; use crate::{generate_doc_comments, naive_snake_case};
use proc_macro2::TokenStream; use proc_macro2::TokenStream;
use prost_build::{Method, Service};
use quote::{format_ident, quote}; use quote::{format_ident, quote};
pub(crate) fn generate(service: &Service, proto: &str) -> TokenStream { /// Generate service for client
let service_ident = quote::format_ident!("{}Client", service.name); pub fn generate<'a, T: Service<'a>>(service: &'a T, context: &T::Context) -> TokenStream {
let client_mod = quote::format_ident!("{}_client", naive_snake_case(&service.name)); let service_ident = quote::format_ident!("{}Client", service.name());
let methods = generate_methods(service, proto); 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 connect = generate_connect(&service_ident);
let service_doc = generate_doc_comments(&service.comments.leading); let service_doc = generate_doc_comments(service.comment());
quote! { quote! {
/// Generated client implementations. /// Generated client implementations.
@@ -75,22 +76,26 @@ fn generate_connect(_service_ident: &syn::Ident) -> TokenStream {
TokenStream::new() 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(); let mut stream = TokenStream::new();
for method in &service.methods { for method in service.methods() {
use super::schema::Commentable;
let path = format!( 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) { let method = match (method.client_streaming(), method.server_streaming()) {
(false, false) => generate_unary(method, &proto, path), (false, false) => generate_unary(method, &context, path),
(false, true) => generate_server_streaming(method, &proto, path), (false, true) => generate_server_streaming(method, &context, path),
(true, false) => generate_client_streaming(method, &proto, path), (true, false) => generate_client_streaming(method, &context, path),
(true, true) => generate_streaming(method, &proto, path), (true, true) => generate_streaming(method, &context, path),
}; };
stream.extend(method); stream.extend(method);
@@ -99,9 +104,14 @@ fn generate_methods(service: &Service, proto: &str) -> TokenStream {
stream stream
} }
fn generate_unary(method: &Method, proto: &str, path: String) -> TokenStream { fn generate_unary<'a, T: Method<'a>>(
let ident = format_ident!("{}", method.name); method: &T,
let (request, response) = crate::replace_wellknown(proto, &method); context: &T::Context,
path: String,
) -> TokenStream {
let codec_name = syn::parse_str::<syn::Path>(context.codec_name()).unwrap();
let ident = format_ident!("{}", method.name());
let (request, response) = method.request_response_name(context);
quote! { quote! {
pub async fn #ident( 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| { self.inner.ready().await.map_err(|e| {
tonic::Status::new(tonic::Code::Unknown, format!("Service was not ready: {}", e.into())) 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); let path = http::uri::PathAndQuery::from_static(#path);
self.inner.unary(request.into_request(), path, codec).await self.inner.unary(request.into_request(), path, codec).await
} }
} }
} }
fn generate_server_streaming(method: &Method, proto: &str, path: String) -> TokenStream { fn generate_server_streaming<'a, T: Method<'a>>(
let ident = format_ident!("{}", method.name); method: &T,
context: &T::Context,
path: String,
) -> TokenStream {
let codec_name = syn::parse_str::<syn::Path>(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! { quote! {
pub async fn #ident( 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| { self.inner.ready().await.map_err(|e| {
tonic::Status::new(tonic::Code::Unknown, format!("Service was not ready: {}", e.into())) 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); let path = http::uri::PathAndQuery::from_static(#path);
self.inner.server_streaming(request.into_request(), path, codec).await self.inner.server_streaming(request.into_request(), path, codec).await
} }
} }
} }
fn generate_client_streaming(method: &Method, proto: &str, path: String) -> TokenStream { fn generate_client_streaming<'a, T: Method<'a>>(
let ident = format_ident!("{}", method.name); method: &T,
context: &T::Context,
path: String,
) -> TokenStream {
let codec_name = syn::parse_str::<syn::Path>(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! { quote! {
pub async fn #ident( 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| { self.inner.ready().await.map_err(|e| {
tonic::Status::new(tonic::Code::Unknown, format!("Service was not ready: {}", e.into())) 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); let path = http::uri::PathAndQuery::from_static(#path);
self.inner.client_streaming(request.into_streaming_request(), path, codec).await self.inner.client_streaming(request.into_streaming_request(), path, codec).await
} }
} }
} }
fn generate_streaming(method: &Method, proto: &str, path: String) -> TokenStream { fn generate_streaming<'a, T: Method<'a>>(
let ident = format_ident!("{}", method.name); method: &T,
context: &T::Context,
path: String,
) -> TokenStream {
let codec_name = syn::parse_str::<syn::Path>(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! { quote! {
pub async fn #ident( 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| { self.inner.ready().await.map_err(|e| {
tonic::Status::new(tonic::Code::Unknown, format!("Service was not ready: {}", e.into())) 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); let path = http::uri::PathAndQuery::from_static(#path);
self.inner.streaming(request.into_streaming_request(), path, codec).await self.inner.streaming(request.into_streaming_request(), path, codec).await
} }
+21 -241
View File
@@ -23,7 +23,7 @@
//! //!
//! ```rust,no_run //! ```rust,no_run
//! fn main() -> Result<(), Box<dyn std::error::Error>> { //! fn main() -> Result<(), Box<dyn std::error::Error>> {
//! tonic_build::compile_protos("proto/service.proto")?; //! tonic_build::prost::compile_protos("proto/service.proto")?;
//! Ok(()) //! Ok(())
//! } //! }
//! ``` //! ```
@@ -32,7 +32,7 @@
//! //!
//! ```rust,no_run //! ```rust,no_run
//! fn main() -> Result<(), Box<dyn std::error::Error>> { //! fn main() -> Result<(), Box<dyn std::error::Error>> {
//! tonic_build::configure() //! tonic_build::prost::configure()
//! .build_server(false) //! .build_server(false)
//! .compile( //! .compile(
//! &["proto/helloworld/helloworld.proto"], //! &["proto/helloworld/helloworld.proto"],
@@ -57,163 +57,25 @@
#![doc(test(no_crate_inject, attr(deny(rust_2018_idioms))))] #![doc(test(no_crate_inject, attr(deny(rust_2018_idioms))))]
use proc_macro2::{Delimiter, Group, Ident, Literal, Punct, Spacing, Span, TokenStream}; use proc_macro2::{Delimiter, Group, Ident, Literal, Punct, Spacing, Span, TokenStream};
use prost_build::{Config, Method}; use quote::TokenStreamExt;
use quote::{ToTokens, TokenStreamExt};
/// Prost generator
#[cfg(feature = "prost")]
pub mod prost;
/// Traits to describe schema
pub mod schema;
#[cfg(feature = "rustfmt")] #[cfg(feature = "rustfmt")]
use std::process::Command; use std::process::Command;
use std::{
io,
path::{Path, PathBuf},
};
mod client; /// Serivce code generation for client
mod server; pub mod client;
/// Serivce code generation for Server
/// Service generator builder. pub mod server;
#[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<PathBuf>,
#[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<Path>) -> 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<str>, rust_path: impl AsRef<str>) -> 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<P: AsRef<str>, A: AsRef<str>>(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<P: AsRef<str>, A: AsRef<str>>(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<P: AsRef<Path>>(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<Path>) -> 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(())
}
/// Format files under the out_dir with rustfmt
#[cfg(feature = "rustfmt")] #[cfg(feature = "rustfmt")]
fn fmt(out_dir: &str) { pub fn fmt(out_dir: &str) {
let dir = std::fs::read_dir(out_dir).unwrap(); let dir = std::fs::read_dir(out_dir).unwrap();
for entry in dir { 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 // Generate a singular line of a doc comment
fn generate_doc_comment(comment: &str) -> TokenStream { fn generate_doc_comment<S: AsRef<str>>(comment: S) -> TokenStream {
let mut doc_stream = TokenStream::new(); let mut doc_stream = TokenStream::new();
doc_stream.append(Ident::new("doc", Span::call_site())); doc_stream.append(Ident::new("doc", Span::call_site()));
doc_stream.append(Punct::new('=', Spacing::Alone)); 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); 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 // Generate a larger doc comment composed of many lines of doc comments
fn generate_doc_comments<T: AsRef<str>>(comments: &[T]) -> TokenStream { fn generate_doc_comments<'a, T: AsRef<str> + 'a, C: IntoIterator<Item = &'a T>>(
comments: C,
) -> TokenStream {
let mut stream = TokenStream::new(); let mut stream = TokenStream::new();
for comment in comments { for comment in comments {
stream.extend(generate_doc_comment(comment.as_ref())); stream.extend(generate_doc_comment(comment));
} }
stream 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::<TokenStream>().unwrap()
} else {
syn::parse_str::<syn::Path>(&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::<TokenStream>().unwrap()
} else {
syn::parse_str::<syn::Path>(&format!("{}::{}", proto_path, method.output_type))
.unwrap()
.to_token_stream()
};
(request, response)
}
fn naive_snake_case(name: &str) -> String { fn naive_snake_case(name: &str) -> String {
let mut s = String::new(); let mut s = String::new();
let mut it = name.chars().peekable(); let mut it = name.chars().peekable();
+320
View File
@@ -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<Self::Comment>;
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<Self::Method>;
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<Self::Comment>;
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::<TokenStream>().unwrap()
} else {
syn::parse_str::<syn::Path>(&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::<TokenStream>().unwrap()
} else {
syn::parse_str::<syn::Path>(&format!("{}::{}", context.proto_path, self.output_type))
.unwrap()
.to_token_stream()
};
(request, response)
}
}
pub(crate) fn compile<P: AsRef<Path>>(
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<PathBuf>,
#[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<Path>) -> 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<str>, rust_path: impl AsRef<str>) -> 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<P: AsRef<str>, A: AsRef<str>>(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<P: AsRef<str>, A: AsRef<str>>(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<P: AsRef<Path>>(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<Path>) -> 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(())
}
+53
View File
@@ -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<str> + 'a;
/// Container has comments.
type CommentContainer: IntoIterator<Item = &'a Self::Comment>;
/// 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<Item = &'a Self::Method>;
/// 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);
}
+72 -58
View File
@@ -1,20 +1,21 @@
use super::schema::{Commentable, Context, Method, Service};
use crate::{generate_doc_comment, generate_doc_comments, naive_snake_case}; use crate::{generate_doc_comment, generate_doc_comments, naive_snake_case};
use proc_macro2::{Span, TokenStream}; use proc_macro2::{Span, TokenStream};
use prost_build::{Method, Service};
use quote::quote; use quote::quote;
use syn::{Ident, Lit, LitStr}; use syn::{Ident, Lit, LitStr};
pub(crate) fn generate(service: &Service, proto_path: &str) -> TokenStream { /// Generate service for Server
let methods = generate_methods(&service, proto_path); 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_service = quote::format_ident!("{}Server", service.name());
let server_trait = quote::format_ident!("{}", service.name); let server_trait = quote::format_ident!("{}", service.name());
let server_mod = quote::format_ident!("{}_server", naive_snake_case(&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 generated_trait = generate_trait(service, context, server_trait.clone());
let service_doc = generate_doc_comments(&service.comments.leading); let service_doc = generate_doc_comments(service.comment());
// Transport based implementations // 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); let transport = generate_transport(&server_service, &server_trait, &path);
quote! { 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 { fn generate_trait<'a, T: Service<'a>>(
let methods = generate_trait_methods(service, proto_path); service: &'a T,
context: &T::Context,
server_trait: Ident,
) -> TokenStream {
let methods = generate_trait_methods(service, context);
let trait_doc = generate_doc_comment(&format!( let trait_doc = generate_doc_comment(&format!(
"Generated trait containing gRPC methods that should be implemented for use with {}Server.", "Generated trait containing gRPC methods that should be implemented for use with {}Server.",
service.name service.name()
)); ));
quote! { 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(); let mut stream = TokenStream::new();
for method in &service.methods { for method in service.methods() {
let name = quote::format_ident!("{}", method.name); 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) => { (false, false) => {
quote! { quote! {
#method_doc #method_doc
@@ -140,10 +145,10 @@ fn generate_trait_methods(service: &Service, proto_path: &str) -> TokenStream {
} }
} }
(false, true) => { (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!( let stream_doc = generate_doc_comment(&format!(
"Server streaming response type for the {} method.", "Server streaming response type for the {} method.",
method.proto_name method.identifier()
)); ));
quote! { quote! {
@@ -156,10 +161,10 @@ fn generate_trait_methods(service: &Service, proto_path: &str) -> TokenStream {
} }
} }
(true, true) => { (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!( let stream_doc = generate_doc_comment(&format!(
"Server streaming response type for the {} method.", "Server streaming response type for the {} method.",
method.proto_name method.identifier()
)); ));
quote! { quote! {
@@ -203,29 +208,31 @@ fn generate_transport(
TokenStream::new() 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(); let mut stream = TokenStream::new();
for method in &service.methods { for method in service.methods() {
let path = format!( 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 method_path = Lit::Str(LitStr::new(&path, Span::call_site()));
let ident = quote::format_ident!("{}", method.name); let ident = quote::format_ident!("{}", method.name());
let server_trait = quote::format_ident!("{}", service.name); let server_trait = quote::format_ident!("{}", service.name());
let method_stream = match (method.client_streaming, method.server_streaming) { let method_stream = match (method.client_streaming(), method.server_streaming()) {
(false, false) => generate_unary(method, ident, proto_path, server_trait), (false, false) => generate_unary(method, ident, context, server_trait),
(false, true) => { (false, true) => {
generate_server_streaming(method, ident.clone(), proto_path, server_trait) generate_server_streaming(method, ident.clone(), context, server_trait)
} }
(true, false) => { (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! { let method = quote! {
@@ -239,15 +246,17 @@ fn generate_methods(service: &Service, proto_path: &str) -> TokenStream {
stream stream
} }
fn generate_unary( fn generate_unary<'a, T: Method<'a>>(
method: &Method, method: &T,
method_ident: Ident, method_ident: Ident,
proto_path: &str, context: &T::Context,
server_trait: Ident, server_trait: Ident,
) -> TokenStream { ) -> TokenStream {
let service_ident = quote::format_ident!("{}Svc", method.proto_name); let codec_name = syn::parse_str::<syn::Path>(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! { quote! {
struct #service_ident<T: #server_trait >(pub Arc<T>); struct #service_ident<T: #server_trait >(pub Arc<T>);
@@ -270,7 +279,7 @@ fn generate_unary(
let interceptor = inner.1.clone(); let interceptor = inner.1.clone();
let inner = inner.0; let inner = inner.0;
let method = #service_ident(inner); let method = #service_ident(inner);
let codec = tonic::codec::ProstCodec::default(); let codec = #codec_name::default();
let mut grpc = if let Some(interceptor) = interceptor { let mut grpc = if let Some(interceptor) = interceptor {
tonic::server::Grpc::with_interceptor(codec, interceptor) tonic::server::Grpc::with_interceptor(codec, interceptor)
@@ -286,17 +295,19 @@ fn generate_unary(
} }
} }
fn generate_server_streaming( fn generate_server_streaming<'a, T: Method<'a>>(
method: &Method, method: &T,
method_ident: Ident, method_ident: Ident,
proto_path: &str, context: &T::Context,
server_trait: Ident, server_trait: Ident,
) -> TokenStream { ) -> TokenStream {
let service_ident = quote::format_ident!("{}Svc", method.proto_name); let codec_name = syn::parse_str::<syn::Path>(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! { quote! {
struct #service_ident<T: #server_trait >(pub Arc<T>); struct #service_ident<T: #server_trait >(pub Arc<T>);
@@ -321,7 +332,7 @@ fn generate_server_streaming(
let interceptor = inner.1; let interceptor = inner.1;
let inner = inner.0; let inner = inner.0;
let method = #service_ident(inner); let method = #service_ident(inner);
let codec = tonic::codec::ProstCodec::default(); let codec = #codec_name::default();
let mut grpc = if let Some(interceptor) = interceptor { let mut grpc = if let Some(interceptor) = interceptor {
tonic::server::Grpc::with_interceptor(codec, interceptor) tonic::server::Grpc::with_interceptor(codec, interceptor)
@@ -337,15 +348,16 @@ fn generate_server_streaming(
} }
} }
fn generate_client_streaming( fn generate_client_streaming<'a, T: Method<'a>>(
method: &Method, method: &T,
method_ident: Ident, method_ident: Ident,
proto_path: &str, context: &T::Context,
server_trait: Ident, server_trait: Ident,
) -> TokenStream { ) -> 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::<syn::Path>(context.codec_name()).unwrap();
quote! { quote! {
struct #service_ident<T: #server_trait >(pub Arc<T>); struct #service_ident<T: #server_trait >(pub Arc<T>);
@@ -370,7 +382,7 @@ fn generate_client_streaming(
let interceptor = inner.1; let interceptor = inner.1;
let inner = inner.0; let inner = inner.0;
let method = #service_ident(inner); let method = #service_ident(inner);
let codec = tonic::codec::ProstCodec::default(); let codec = #codec_name::default();
let mut grpc = if let Some(interceptor) = interceptor { let mut grpc = if let Some(interceptor) = interceptor {
tonic::server::Grpc::with_interceptor(codec, interceptor) tonic::server::Grpc::with_interceptor(codec, interceptor)
@@ -386,17 +398,19 @@ fn generate_client_streaming(
} }
} }
fn generate_streaming( fn generate_streaming<'a, T: Method<'a>>(
method: &Method, method: &T,
method_ident: Ident, method_ident: Ident,
proto_path: &str, context: &T::Context,
server_trait: Ident, server_trait: Ident,
) -> TokenStream { ) -> TokenStream {
let service_ident = quote::format_ident!("{}Svc", method.proto_name); let codec_name = syn::parse_str::<syn::Path>(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! { quote! {
struct #service_ident<T: #server_trait>(pub Arc<T>); struct #service_ident<T: #server_trait>(pub Arc<T>);
@@ -421,7 +435,7 @@ fn generate_streaming(
let interceptor = inner.1; let interceptor = inner.1;
let inner = inner.0; let inner = inner.0;
let method = #service_ident(inner); let method = #service_ident(inner);
let codec = tonic::codec::ProstCodec::default(); let codec = #codec_name::default();
let mut grpc = if let Some(interceptor) = interceptor { let mut grpc = if let Some(interceptor) = interceptor {
tonic::server::Grpc::with_interceptor(codec, interceptor) tonic::server::Grpc::with_interceptor(codec, interceptor)
+3 -2
View File
@@ -23,8 +23,8 @@ categories = ["web-programming", "network-programming", "asynchronous"]
keywords = ["rpc", "grpc", "async", "futures", "protobuf"] keywords = ["rpc", "grpc", "async", "futures", "protobuf"]
[features] [features]
default = ["transport", "codegen"] default = ["transport", "codegen", "data-prost"]
codegen = ["async-trait", "prost", "prost-derive"] codegen = ["async-trait"]
transport = [ transport = [
"hyper", "hyper",
"tokio", "tokio",
@@ -35,6 +35,7 @@ transport = [
] ]
tls = ["transport", "tokio-rustls"] tls = ["transport", "tokio-rustls"]
tls-roots = ["tls", "rustls-native-certs"] tls-roots = ["tls", "rustls-native-certs"]
data-prost = ["prost", "prost-derive"]
# [[bench]] # [[bench]]
# name = "bench_main" # name = "bench_main"
+5 -5
View File
@@ -6,18 +6,18 @@
mod buffer; mod buffer;
mod decode; mod decode;
mod encode; mod encode;
#[cfg(feature = "prost")] #[cfg(feature = "data-prost")]
mod prost; mod prost;
#[cfg(test)] #[cfg(all(test, feature = "data-prost"))]
mod tests; mod prost_tests;
use std::io; use std::io;
pub use self::decode::Streaming; pub use self::decode::Streaming;
pub(crate) use self::encode::{encode_client, encode_server}; pub(crate) use self::encode::{encode_client, encode_server};
#[cfg(feature = "prost")] #[cfg(feature = "data-prost")]
#[cfg_attr(docsrs, doc(cfg(feature = "prost")))] #[cfg_attr(docsrs, doc(cfg(feature = "data-prost")))]
pub use self::prost::ProstCodec; pub use self::prost::ProstCodec;
use crate::Status; use crate::Status;
pub use buffer::{DecodeBuf, EncodeBuf}; pub use buffer::{DecodeBuf, EncodeBuf};