Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
367 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,365 @@ | ||
use super::{client, server, Attributes}; | ||
use proc_macro2::TokenStream; | ||
use quote::ToTokens; | ||
use std::{ | ||
ffi::OsString, | ||
fs, io, | ||
path::{Path, PathBuf}, | ||
}; | ||
|
||
#[derive(Debug, Default)] | ||
pub struct ServiceBuilder { | ||
/// The service name in Rust style. | ||
pub name: Option<String>, | ||
/// The service name as it appears in the .proto file. | ||
pub proto_name: Option<String>, | ||
/// The package name as it appears in the .proto file. | ||
pub package: Option<String>, | ||
/// The service comments. | ||
pub comments: Vec<String>, | ||
/// The service methods. | ||
pub methods: Vec<Method>, | ||
} | ||
|
||
impl ServiceBuilder { | ||
pub fn name<T: AsRef<str>>(mut self, name: T) -> Self { | ||
self.name = Some(name.as_ref().to_owned()); | ||
self | ||
} | ||
|
||
pub fn package<T: AsRef<str>>(mut self, package: T) -> Self { | ||
self.package = Some(package.as_ref().to_owned()); | ||
self | ||
} | ||
|
||
pub fn method(mut self, method: Method) -> Self { | ||
self.methods.push(method); | ||
self | ||
} | ||
|
||
pub fn build(self) -> Service { | ||
Service { | ||
name: self.name.unwrap(), | ||
comments: self.comments, | ||
package: self.package.unwrap(), | ||
methods: self.methods, | ||
} | ||
} | ||
} | ||
|
||
/// A service descriptor. | ||
#[derive(Debug)] | ||
pub struct Service { | ||
/// The service name in Rust style. | ||
pub name: String, | ||
/// The package name as it appears in the .proto file. | ||
pub package: String, | ||
/// The service comments. | ||
pub comments: Vec<String>, | ||
/// The service methods. | ||
pub methods: Vec<Method>, | ||
} | ||
|
||
impl Service { | ||
pub fn builder() -> ServiceBuilder { | ||
ServiceBuilder::default() | ||
} | ||
} | ||
|
||
impl crate::Service for Service { | ||
type Comment = String; | ||
|
||
type Method = Method; | ||
|
||
fn name(&self) -> &str { | ||
&self.name | ||
} | ||
|
||
fn package(&self) -> &str { | ||
&self.package | ||
} | ||
|
||
fn identifier(&self) -> &str { | ||
&self.name | ||
} | ||
|
||
fn methods(&self) -> &[Self::Method] { | ||
&self.methods | ||
} | ||
|
||
fn comment(&self) -> &[Self::Comment] { | ||
&self.comments | ||
} | ||
} | ||
|
||
/// A service method descriptor. | ||
#[derive(Debug)] | ||
pub struct Method { | ||
/// The name of the method in Rust style. | ||
pub name: String, | ||
/// The name of the method as should be used when constructing a route | ||
pub proto_name: String, | ||
/// The method comments. | ||
pub comments: Vec<String>, | ||
/// The input Rust type. | ||
pub input_type: String, | ||
/// The output Rust type. | ||
pub output_type: String, | ||
/// Identifies if client streams multiple client messages. | ||
pub client_streaming: bool, | ||
/// Identifies if server streams multiple server messages. | ||
pub server_streaming: bool, | ||
/// The path to the codec to use for this method | ||
pub codec_path: String, | ||
} | ||
|
||
impl Method { | ||
pub fn builder() -> MethodBuilder { | ||
MethodBuilder::default() | ||
} | ||
} | ||
|
||
impl crate::Method for Method { | ||
type Comment = String; | ||
|
||
fn name(&self) -> &str { | ||
&self.name | ||
} | ||
|
||
fn identifier(&self) -> &str { | ||
&self.proto_name | ||
} | ||
|
||
fn codec_path(&self) -> &str { | ||
&self.codec_path | ||
} | ||
|
||
fn client_streaming(&self) -> bool { | ||
self.client_streaming | ||
} | ||
|
||
fn server_streaming(&self) -> bool { | ||
self.server_streaming | ||
} | ||
|
||
fn comment(&self) -> &[Self::Comment] { | ||
&self.comments | ||
} | ||
|
||
fn request_response_name( | ||
&self, | ||
_proto_path: &str, | ||
_compile_well_known_types: bool, | ||
) -> (TokenStream, TokenStream) { | ||
let request = syn::parse_str::<syn::Path>(&self.input_type) | ||
.unwrap() | ||
.to_token_stream(); | ||
let response = syn::parse_str::<syn::Path>(&self.output_type) | ||
.unwrap() | ||
.to_token_stream(); | ||
(request, response) | ||
} | ||
} | ||
|
||
#[derive(Debug, Default)] | ||
pub struct MethodBuilder { | ||
/// The name of the method in Rust style. | ||
pub name: Option<String>, | ||
/// The name of the method as should be used when constructing a route | ||
pub proto_name: Option<String>, | ||
/// The method comments. | ||
pub comments: Vec<String>, | ||
/// The input Rust type. | ||
pub input_type: Option<String>, | ||
/// The output Rust type. | ||
pub output_type: Option<String>, | ||
/// Identifies if client streams multiple client messages. | ||
pub client_streaming: bool, | ||
/// Identifies if server streams multiple server messages. | ||
pub server_streaming: bool, | ||
/// The path to the codec to use for this method | ||
pub codec_path: Option<String>, | ||
} | ||
|
||
impl MethodBuilder { | ||
pub fn name<T: AsRef<str>>(mut self, name: T) -> Self { | ||
self.name = Some(name.as_ref().to_owned()); | ||
self | ||
} | ||
|
||
pub fn proto_name<T: AsRef<str>>(mut self, proto_name: T) -> Self { | ||
self.proto_name = Some(proto_name.as_ref().to_owned()); | ||
self | ||
} | ||
|
||
pub fn input_type<T: AsRef<str>>(mut self, input_type: T) -> Self { | ||
self.input_type = Some(input_type.as_ref().to_owned()); | ||
self | ||
} | ||
|
||
pub fn output_type<T: AsRef<str>>(mut self, output_type: T) -> Self { | ||
self.output_type = Some(output_type.as_ref().to_owned()); | ||
self | ||
} | ||
|
||
pub fn codec_path<T: AsRef<str>>(mut self, codec_path: T) -> Self { | ||
self.codec_path = Some(codec_path.as_ref().to_owned()); | ||
self | ||
} | ||
|
||
pub fn client_streaming(mut self) -> Self { | ||
self.client_streaming = true; | ||
self | ||
} | ||
|
||
pub fn server_streaming(mut self) -> Self { | ||
self.server_streaming = true; | ||
self | ||
} | ||
|
||
pub fn build(self) -> Method { | ||
Method { | ||
name: self.name.unwrap(), | ||
proto_name: self.proto_name.unwrap(), | ||
comments: self.comments, | ||
input_type: self.input_type.unwrap(), | ||
output_type: self.output_type.unwrap(), | ||
client_streaming: self.client_streaming, | ||
server_streaming: self.server_streaming, | ||
codec_path: self.codec_path.unwrap(), | ||
} | ||
} | ||
} | ||
|
||
struct ServiceGenerator { | ||
builder: Builder, | ||
clients: TokenStream, | ||
servers: TokenStream, | ||
} | ||
|
||
impl ServiceGenerator { | ||
fn generate(&mut self, service: &Service) { | ||
if self.builder.build_server { | ||
let server = server::generate( | ||
service, | ||
true, // emit_package, | ||
"", // proto_path, -- not used | ||
false, // compile_well_known_types -- not used | ||
&Attributes::default(), | ||
); | ||
self.servers.extend(server); | ||
} | ||
|
||
if self.builder.build_client { | ||
let client = client::generate( | ||
service, | ||
true, // emit_package, | ||
"", // proto_path, -- not used | ||
false, // compile_well_known_types, -- not used | ||
&Attributes::default(), | ||
); | ||
self.clients.extend(client); | ||
} | ||
} | ||
|
||
fn finalize(&mut self, buf: &mut String) { | ||
if self.builder.build_client && !self.clients.is_empty() { | ||
let clients = &self.clients; | ||
|
||
let client_service = quote::quote! { | ||
#clients | ||
}; | ||
|
||
let ast: syn::File = syn::parse2(client_service).expect("not a valid tokenstream"); | ||
let code = prettyplease::unparse(&ast); | ||
buf.push_str(&code); | ||
|
||
self.clients = TokenStream::default(); | ||
} | ||
|
||
if self.builder.build_server && !self.servers.is_empty() { | ||
let servers = &self.servers; | ||
|
||
let server_service = quote::quote! { | ||
#servers | ||
}; | ||
|
||
let ast: syn::File = syn::parse2(server_service).expect("not a valid tokenstream"); | ||
let code = prettyplease::unparse(&ast); | ||
buf.push_str(&code); | ||
|
||
self.servers = TokenStream::default(); | ||
} | ||
} | ||
} | ||
|
||
struct Builder { | ||
build_server: bool, | ||
build_client: bool, | ||
|
||
out_dir: Option<PathBuf>, | ||
} | ||
|
||
impl Builder { | ||
/// 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 | ||
} | ||
|
||
pub fn compile(self, services: &[Service]) { | ||
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()) | ||
}; | ||
|
||
let mut generator = ServiceGenerator { | ||
builder: self, | ||
clients: TokenStream::default(), | ||
servers: TokenStream::default(), | ||
}; | ||
|
||
for service in services { | ||
generator.generate(service); | ||
} | ||
|
||
let mut output = String::new(); | ||
generator.finalize(&mut output); | ||
|
||
//TODO group services into packages and put all packages into separate files | ||
let out_file = out_dir.join("tonic.rs"); | ||
fs::write(out_file, output).unwrap(); | ||
} | ||
} | ||
|
||
#[test] | ||
fn rust_build() { | ||
let method = Method::builder() | ||
.name("say_hello") | ||
.proto_name("SayHello") | ||
.input_type("super::HelloRequest") | ||
.output_type("super::HelloResponse") | ||
.codec_path("crate::BincodeCodec") | ||
.build(); | ||
|
||
let service = Service::builder() | ||
.name("Greeter") | ||
.package("helloworld") | ||
.method(method) | ||
.build(); | ||
|
||
let out_dir = PathBuf::from(std::env!("CARGO_MANIFEST_DIR")).join("src"); | ||
// .join("generated"); | ||
|
||
let mut builder = Builder { | ||
build_server: true, | ||
build_client: true, | ||
out_dir: None, | ||
} | ||
.out_dir(out_dir); | ||
|
||
builder.compile(&[service]); | ||
} |