492 lines
14 KiB
Rust
492 lines
14 KiB
Rust
//! This module provides utilities for generating `tonic` service stubs and clients
|
|
//! purely in Rust without the need of `proto` files. It also enables you to set a custom `Codec`
|
|
//! if you want to use a custom serialization format other than `protobuf`.
|
|
//!
|
|
//! # Example
|
|
//!
|
|
//! ```rust,no_run
|
|
//! fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
//! let greeter_service = tonic_build::manual::Service::builder()
|
|
//! .name("Greeter")
|
|
//! .package("helloworld")
|
|
//! .method(
|
|
//! tonic_build::manual::Method::builder()
|
|
//! .name("say_hello")
|
|
//! .route_name("SayHello")
|
|
//! // Provide the path to the Request type
|
|
//! .input_type("crate::HelloRequest")
|
|
//! // Provide the path to the Response type
|
|
//! .output_type("super::HelloResponse")
|
|
//! // Provide the path to the Codec to use
|
|
//! .codec_path("crate::JsonCodec")
|
|
//! .build(),
|
|
//! )
|
|
//! .build();
|
|
//!
|
|
//! tonic_build::manual::Builder::new().compile(&[greeter_service]);
|
|
//! Ok(())
|
|
//! }
|
|
//! ```
|
|
|
|
use crate::code_gen::CodeGenBuilder;
|
|
|
|
use proc_macro2::TokenStream;
|
|
use quote::ToTokens;
|
|
use std::{
|
|
fs,
|
|
path::{Path, PathBuf},
|
|
};
|
|
|
|
/// Service builder.
|
|
///
|
|
/// This builder can be used to manually define a gRPC service in rust code without the use of a
|
|
/// .proto file.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```
|
|
/// # use tonic_build::manual::Service;
|
|
/// let greeter_service = Service::builder()
|
|
/// .name("Greeter")
|
|
/// .package("helloworld")
|
|
/// // Add various methods to the service
|
|
/// // .method()
|
|
/// .build();
|
|
/// ```
|
|
#[derive(Debug, Default)]
|
|
pub struct ServiceBuilder {
|
|
/// The service name in Rust style.
|
|
name: Option<String>,
|
|
/// The package name as it appears in the .proto file.
|
|
package: Option<String>,
|
|
/// The service comments.
|
|
comments: Vec<String>,
|
|
/// The service methods.
|
|
methods: Vec<Method>,
|
|
}
|
|
|
|
impl ServiceBuilder {
|
|
/// Set the name for this Service.
|
|
///
|
|
/// This value will be used both as the base for the generated rust types and service trait as
|
|
/// well as part of the route for calling this service. Routes have the form:
|
|
/// `/<package_name>.<service_name>/<method_route_name>`
|
|
pub fn name(mut self, name: impl AsRef<str>) -> Self {
|
|
self.name = Some(name.as_ref().to_owned());
|
|
self
|
|
}
|
|
|
|
/// Set the package this Service is part of.
|
|
///
|
|
/// This value will be used as part of the route for calling this service.
|
|
/// Routes have the form: `/<package_name>.<service_name>/<method_route_name>`
|
|
pub fn package(mut self, package: impl AsRef<str>) -> Self {
|
|
self.package = Some(package.as_ref().to_owned());
|
|
self
|
|
}
|
|
|
|
/// Add a comment string that should be included as a doc comment for this Service.
|
|
pub fn comment(mut self, comment: impl AsRef<str>) -> Self {
|
|
self.comments.push(comment.as_ref().to_owned());
|
|
self
|
|
}
|
|
|
|
/// Adds a Method to this Service.
|
|
pub fn method(mut self, method: Method) -> Self {
|
|
self.methods.push(method);
|
|
self
|
|
}
|
|
|
|
/// Build a Service.
|
|
///
|
|
/// Panics if `name` or `package` weren't set.
|
|
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.
|
|
name: String,
|
|
/// The package name as it appears in the .proto file.
|
|
package: String,
|
|
/// The service comments.
|
|
comments: Vec<String>,
|
|
/// The service methods.
|
|
methods: Vec<Method>,
|
|
}
|
|
|
|
impl Service {
|
|
/// Create a new `ServiceBuilder`
|
|
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.
|
|
name: String,
|
|
/// The name of the method as should be used when constructing a route
|
|
route_name: String,
|
|
/// The method comments.
|
|
comments: Vec<String>,
|
|
/// The input Rust type.
|
|
input_type: String,
|
|
/// The output Rust type.
|
|
output_type: String,
|
|
/// Identifies if client streams multiple client messages.
|
|
client_streaming: bool,
|
|
/// Identifies if server streams multiple server messages.
|
|
server_streaming: bool,
|
|
/// The path to the codec to use for this method
|
|
codec_path: String,
|
|
}
|
|
|
|
impl Method {
|
|
/// Create a new `MethodBuilder`
|
|
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.route_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)
|
|
}
|
|
}
|
|
|
|
/// Method builder.
|
|
///
|
|
/// This builder can be used to manually define gRPC method, which can be added to a gRPC service,
|
|
/// in rust code without the use of a .proto file.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```
|
|
/// # use tonic_build::manual::Method;
|
|
/// let say_hello_method = Method::builder()
|
|
/// .name("say_hello")
|
|
/// .route_name("SayHello")
|
|
/// // Provide the path to the Request type
|
|
/// .input_type("crate::common::HelloRequest")
|
|
/// // Provide the path to the Response type
|
|
/// .output_type("crate::common::HelloResponse")
|
|
/// // Provide the path to the Codec to use
|
|
/// .codec_path("crate::common::JsonCodec")
|
|
/// .build();
|
|
/// ```
|
|
#[derive(Debug, Default)]
|
|
pub struct MethodBuilder {
|
|
/// The name of the method in Rust style.
|
|
name: Option<String>,
|
|
/// The name of the method as should be used when constructing a route
|
|
route_name: Option<String>,
|
|
/// The method comments.
|
|
comments: Vec<String>,
|
|
/// The input Rust type.
|
|
input_type: Option<String>,
|
|
/// The output Rust type.
|
|
output_type: Option<String>,
|
|
/// Identifies if client streams multiple client messages.
|
|
client_streaming: bool,
|
|
/// Identifies if server streams multiple server messages.
|
|
server_streaming: bool,
|
|
/// The path to the codec to use for this method
|
|
codec_path: Option<String>,
|
|
}
|
|
|
|
impl MethodBuilder {
|
|
/// Set the name for this Method.
|
|
///
|
|
/// This value will be used for generating the client functions for calling this Method.
|
|
///
|
|
/// Generally this is formatted in snake_case.
|
|
pub fn name(mut self, name: impl AsRef<str>) -> Self {
|
|
self.name = Some(name.as_ref().to_owned());
|
|
self
|
|
}
|
|
|
|
/// Set the route_name for this Method.
|
|
///
|
|
/// This value will be used as part of the route for calling this method.
|
|
/// Routes have the form: `/<package_name>.<service_name>/<method_route_name>`
|
|
///
|
|
/// Generally this is formatted in PascalCase.
|
|
pub fn route_name(mut self, route_name: impl AsRef<str>) -> Self {
|
|
self.route_name = Some(route_name.as_ref().to_owned());
|
|
self
|
|
}
|
|
|
|
/// Add a comment string that should be included as a doc comment for this Method.
|
|
pub fn comment(mut self, comment: impl AsRef<str>) -> Self {
|
|
self.comments.push(comment.as_ref().to_owned());
|
|
self
|
|
}
|
|
|
|
/// Set the path to the Rust type that should be use for the Request type of this method.
|
|
pub fn input_type(mut self, input_type: impl AsRef<str>) -> Self {
|
|
self.input_type = Some(input_type.as_ref().to_owned());
|
|
self
|
|
}
|
|
|
|
/// Set the path to the Rust type that should be use for the Response type of this method.
|
|
pub fn output_type(mut self, output_type: impl AsRef<str>) -> Self {
|
|
self.output_type = Some(output_type.as_ref().to_owned());
|
|
self
|
|
}
|
|
|
|
/// Set the path to the Rust type that should be used as the `Codec` for this method.
|
|
///
|
|
/// Currently the codegen assumes that this type implements `Default`.
|
|
pub fn codec_path(mut self, codec_path: impl AsRef<str>) -> Self {
|
|
self.codec_path = Some(codec_path.as_ref().to_owned());
|
|
self
|
|
}
|
|
|
|
/// Sets if the Method request from the client is streamed.
|
|
pub fn client_streaming(mut self) -> Self {
|
|
self.client_streaming = true;
|
|
self
|
|
}
|
|
|
|
/// Sets if the Method response from the server is streamed.
|
|
pub fn server_streaming(mut self) -> Self {
|
|
self.server_streaming = true;
|
|
self
|
|
}
|
|
|
|
/// Build a Method
|
|
///
|
|
/// Panics if `name`, `route_name`, `input_type`, `output_type`, or `codec_path` weren't set.
|
|
pub fn build(self) -> Method {
|
|
Method {
|
|
name: self.name.unwrap(),
|
|
route_name: self.route_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 = CodeGenBuilder::new()
|
|
.emit_package(true)
|
|
.compile_well_known_types(false)
|
|
.generate_server(service, "");
|
|
|
|
self.servers.extend(server);
|
|
}
|
|
|
|
if self.builder.build_client {
|
|
let client = CodeGenBuilder::new()
|
|
.emit_package(true)
|
|
.compile_well_known_types(false)
|
|
.build_transport(self.builder.build_transport)
|
|
.generate_client(service, "");
|
|
|
|
self.clients.extend(client);
|
|
}
|
|
}
|
|
|
|
fn finalize(&mut self, buf: &mut String) {
|
|
if self.builder.build_client && !self.clients.is_empty() {
|
|
let clients = &self.clients;
|
|
|
|
let client_service = quote::quote! {
|
|
#clients
|
|
};
|
|
|
|
let ast: syn::File = syn::parse2(client_service).expect("not a valid tokenstream");
|
|
let code = prettyplease::unparse(&ast);
|
|
buf.push_str(&code);
|
|
|
|
self.clients = TokenStream::default();
|
|
}
|
|
|
|
if self.builder.build_server && !self.servers.is_empty() {
|
|
let servers = &self.servers;
|
|
|
|
let server_service = quote::quote! {
|
|
#servers
|
|
};
|
|
|
|
let ast: syn::File = syn::parse2(server_service).expect("not a valid tokenstream");
|
|
let code = prettyplease::unparse(&ast);
|
|
buf.push_str(&code);
|
|
|
|
self.servers = TokenStream::default();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Service generator builder.
|
|
#[derive(Debug)]
|
|
pub struct Builder {
|
|
build_server: bool,
|
|
build_client: bool,
|
|
build_transport: bool,
|
|
|
|
out_dir: Option<PathBuf>,
|
|
}
|
|
|
|
impl Default for Builder {
|
|
fn default() -> Self {
|
|
Self {
|
|
build_server: true,
|
|
build_client: true,
|
|
build_transport: true,
|
|
out_dir: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Builder {
|
|
/// Create a new Builder
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
/// Enable or disable gRPC client code generation.
|
|
///
|
|
/// Defaults to enabling client code generation.
|
|
pub fn build_client(mut self, enable: bool) -> Self {
|
|
self.build_client = enable;
|
|
self
|
|
}
|
|
|
|
/// Enable or disable gRPC server code generation.
|
|
///
|
|
/// Defaults to enabling server code generation.
|
|
pub fn build_server(mut self, enable: bool) -> Self {
|
|
self.build_server = enable;
|
|
self
|
|
}
|
|
|
|
/// Enable or disable generated clients and servers to have built-in tonic
|
|
/// transport features.
|
|
///
|
|
/// When the `transport` feature is disabled this does nothing.
|
|
pub fn build_transport(mut self, enable: bool) -> Self {
|
|
self.build_transport = enable;
|
|
self
|
|
}
|
|
|
|
/// 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
|
|
}
|
|
|
|
/// Performs code generation for the provided services.
|
|
///
|
|
/// Generated services will be output into the directory specified by `out_dir`
|
|
/// with files named `<package_name>.<service_name>.rs`.
|
|
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);
|
|
|
|
let out_file = out_dir.join(format!("{}.{}.rs", service.package, service.name));
|
|
fs::write(out_file, output).unwrap();
|
|
}
|
|
}
|
|
}
|