//! 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> { //! 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, /// The package name as it appears in the .proto file. package: Option, /// The service comments. comments: Vec, /// The service methods. methods: Vec, } 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: /// `/./` pub fn name(mut self, name: impl AsRef) -> 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: `/./` pub fn package(mut self, package: impl AsRef) -> 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) -> 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, /// The service methods. methods: Vec, } 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, /// 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::(&self.input_type) .unwrap() .to_token_stream(); let response = syn::parse_str::(&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, /// The name of the method as should be used when constructing a route route_name: Option, /// The method comments. comments: Vec, /// The input Rust type. input_type: Option, /// The output Rust type. output_type: Option, /// 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, } 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) -> 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: `/./` /// /// Generally this is formatted in PascalCase. pub fn route_name(mut self, route_name: impl AsRef) -> 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) -> 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) -> 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) -> 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) -> 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, } 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) -> 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 `..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(); } } }