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"
[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"] }
+4 -4
View File
@@ -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();
}
+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
fn main() -> Result<(), Box<dyn std::error::Error>> {
tonic_build::compile_protos("proto/helloworld.proto")?;
tonic_build::prost::compile_protos("proto/helloworld.proto")?;
Ok(())
}
```
+2 -2
View File
@@ -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"])
+1 -1
View File
@@ -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"] }
+1 -1
View File
@@ -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);
+1 -1
View File
@@ -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")
+1 -1
View File
@@ -1,3 +1,3 @@
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() {
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() {
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]
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
+2 -2
View File
@@ -23,7 +23,7 @@ tonic-build = <tonic-version>
```rust
fn main() -> Result<(), Box<dyn std::error::Error>> {
tonic_build::compile_protos("proto/service.proto")?;
tonic_build::prost::compile_protos("proto/service.proto")?;
Ok(())
}
```
@@ -32,7 +32,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
```rust
fn main() -> Result<(), Box<dyn std::error::Error>> {
tonic_build::configure()
tonic_build::prost::configure()
.build_server(false)
.compile(
&["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 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::<syn::Path>(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::<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! {
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::<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! {
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::<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! {
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
}
+21 -241
View File
@@ -23,7 +23,7 @@
//!
//! ```rust,no_run
//! fn main() -> Result<(), Box<dyn std::error::Error>> {
//! 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<dyn std::error::Error>> {
//! 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<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(())
}
/// 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<S: AsRef<str>>(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<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();
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::<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 {
let mut s = String::new();
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 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::<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! {
struct #service_ident<T: #server_trait >(pub Arc<T>);
@@ -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::<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! {
struct #service_ident<T: #server_trait >(pub Arc<T>);
@@ -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::<syn::Path>(context.codec_name()).unwrap();
quote! {
struct #service_ident<T: #server_trait >(pub Arc<T>);
@@ -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::<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! {
struct #service_ident<T: #server_trait>(pub Arc<T>);
@@ -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)
+3 -2
View File
@@ -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"
+5 -5
View File
@@ -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};