diff --git a/tonic-build/src/lib.rs b/tonic-build/src/lib.rs index e6c11916a..609de24f4 100644 --- a/tonic-build/src/lib.rs +++ b/tonic-build/src/lib.rs @@ -79,6 +79,8 @@ mod prost; #[cfg_attr(docsrs, doc(cfg(feature = "prost")))] pub use prost::{compile_protos, configure, Builder}; +pub mod manual; + /// Service code generation for client pub mod client; /// Service code generation for Server diff --git a/tonic-build/src/manual.rs b/tonic-build/src/manual.rs new file mode 100644 index 000000000..6aa8d5c2f --- /dev/null +++ b/tonic-build/src/manual.rs @@ -0,0 +1,444 @@ +//! 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 super::{client, server, Attributes}; +use proc_macro2::TokenStream; +use quote::ToTokens; +use std::{ + fs, + path::{Path, PathBuf}, +}; + +/// Service builder. +#[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: T) -> 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: T) -> 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: T) -> 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. +#[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: T) -> 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: T) -> 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: T) -> 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: T) -> 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: T) -> 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: T) -> 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 = 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(); + } + } +} + +/// Service generator builder. +#[derive(Debug)] +pub struct Builder { + build_server: bool, + build_client: bool, + + out_dir: Option, +} + +impl Default for Builder { + fn default() -> Self { + Self { + build_server: true, + build_client: true, + out_dir: None, + } + } +} + +impl Builder { + /// Create a new Builder + pub fn new() -> Self { + Self::default() + } + + /// 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 + } + + /// 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(); + } + } +}