tonic_build/
manual.rs

1//! This module provides utilities for generating `tonic` service stubs and clients
2//! purely in Rust without the need of `proto` files. It also enables you to set a custom `Codec`
3//! if you want to use a custom serialization format other than `protobuf`.
4//!
5//! # Example
6//!
7//! ```rust,no_run
8//! fn main() -> Result<(), Box<dyn std::error::Error>> {
9//!     let greeter_service = tonic_build::manual::Service::builder()
10//!         .name("Greeter")
11//!         .package("helloworld")
12//!         .method(
13//!             tonic_build::manual::Method::builder()
14//!                 .name("say_hello")
15//!                 .route_name("SayHello")
16//!                 // Provide the path to the Request type
17//!                 .input_type("crate::HelloRequest")
18//!                 // Provide the path to the Response type
19//!                 .output_type("super::HelloResponse")
20//!                 // Provide the path to the Codec to use
21//!                 .codec_path("crate::JsonCodec")
22//!                 .build(),
23//!         )
24//!         .build();
25//!
26//!     tonic_build::manual::Builder::new().compile(&[greeter_service]);
27//!     Ok(())
28//! }
29//! ```
30
31use crate::code_gen::CodeGenBuilder;
32
33use proc_macro2::TokenStream;
34use quote::ToTokens;
35use std::{
36    fs,
37    path::{Path, PathBuf},
38};
39
40/// Service builder.
41///
42/// This builder can be used to manually define a gRPC service in rust code without the use of a
43/// .proto file.
44///
45/// # Example
46///
47/// ```
48/// # use tonic_build::manual::Service;
49/// let greeter_service = Service::builder()
50///     .name("Greeter")
51///     .package("helloworld")
52///     // Add various methods to the service
53///     // .method()
54///     .build();
55/// ```
56#[derive(Debug, Default)]
57pub struct ServiceBuilder {
58    /// The service name in Rust style.
59    name: Option<String>,
60    /// The package name as it appears in the .proto file.
61    package: Option<String>,
62    /// The service comments.
63    comments: Vec<String>,
64    /// The service methods.
65    methods: Vec<Method>,
66}
67
68impl ServiceBuilder {
69    /// Set the name for this Service.
70    ///
71    /// This value will be used both as the base for the generated rust types and service trait as
72    /// well as part of the route for calling this service. Routes have the form:
73    /// `/<package_name>.<service_name>/<method_route_name>`
74    pub fn name(mut self, name: impl AsRef<str>) -> Self {
75        self.name = Some(name.as_ref().to_owned());
76        self
77    }
78
79    /// Set the package this Service is part of.
80    ///
81    /// This value will be used as part of the route for calling this service.
82    /// Routes have the form: `/<package_name>.<service_name>/<method_route_name>`
83    pub fn package(mut self, package: impl AsRef<str>) -> Self {
84        self.package = Some(package.as_ref().to_owned());
85        self
86    }
87
88    /// Add a comment string that should be included as a doc comment for this Service.
89    pub fn comment(mut self, comment: impl AsRef<str>) -> Self {
90        self.comments.push(comment.as_ref().to_owned());
91        self
92    }
93
94    /// Adds a Method to this Service.
95    pub fn method(mut self, method: Method) -> Self {
96        self.methods.push(method);
97        self
98    }
99
100    /// Build a Service.
101    ///
102    /// Panics if `name` or `package` weren't set.
103    pub fn build(self) -> Service {
104        Service {
105            name: self.name.unwrap(),
106            comments: self.comments,
107            package: self.package.unwrap(),
108            methods: self.methods,
109        }
110    }
111}
112
113/// A service descriptor.
114#[derive(Debug)]
115pub struct Service {
116    /// The service name in Rust style.
117    name: String,
118    /// The package name as it appears in the .proto file.
119    package: String,
120    /// The service comments.
121    comments: Vec<String>,
122    /// The service methods.
123    methods: Vec<Method>,
124}
125
126impl Service {
127    /// Create a new `ServiceBuilder`
128    pub fn builder() -> ServiceBuilder {
129        ServiceBuilder::default()
130    }
131}
132
133impl crate::Service for Service {
134    type Comment = String;
135
136    type Method = Method;
137
138    fn name(&self) -> &str {
139        &self.name
140    }
141
142    fn package(&self) -> &str {
143        &self.package
144    }
145
146    fn identifier(&self) -> &str {
147        &self.name
148    }
149
150    fn methods(&self) -> &[Self::Method] {
151        &self.methods
152    }
153
154    fn comment(&self) -> &[Self::Comment] {
155        &self.comments
156    }
157}
158
159/// A service method descriptor.
160#[derive(Debug)]
161pub struct Method {
162    /// The name of the method in Rust style.
163    name: String,
164    /// The name of the method as should be used when constructing a route
165    route_name: String,
166    /// The method comments.
167    comments: Vec<String>,
168    /// The input Rust type.
169    input_type: String,
170    /// The output Rust type.
171    output_type: String,
172    /// Identifies if client streams multiple client messages.
173    client_streaming: bool,
174    /// Identifies if server streams multiple server messages.
175    server_streaming: bool,
176    /// Identifies if the method is deprecated.
177    deprecated: bool,
178    /// The path to the codec to use for this method
179    codec_path: String,
180}
181
182impl Method {
183    /// Create a new `MethodBuilder`
184    pub fn builder() -> MethodBuilder {
185        MethodBuilder::default()
186    }
187}
188
189impl crate::Method for Method {
190    type Comment = String;
191
192    fn name(&self) -> &str {
193        &self.name
194    }
195
196    fn identifier(&self) -> &str {
197        &self.route_name
198    }
199
200    fn codec_path(&self) -> &str {
201        &self.codec_path
202    }
203
204    fn client_streaming(&self) -> bool {
205        self.client_streaming
206    }
207
208    fn server_streaming(&self) -> bool {
209        self.server_streaming
210    }
211
212    fn comment(&self) -> &[Self::Comment] {
213        &self.comments
214    }
215
216    fn deprecated(&self) -> bool {
217        self.deprecated
218    }
219
220    fn request_response_name(
221        &self,
222        _proto_path: &str,
223        _compile_well_known_types: bool,
224    ) -> (TokenStream, TokenStream) {
225        let request = syn::parse_str::<syn::Path>(&self.input_type)
226            .unwrap()
227            .to_token_stream();
228        let response = syn::parse_str::<syn::Path>(&self.output_type)
229            .unwrap()
230            .to_token_stream();
231        (request, response)
232    }
233}
234
235/// Method builder.
236///
237/// This builder can be used to manually define gRPC method, which can be added to a gRPC service,
238/// in rust code without the use of a .proto file.
239///
240/// # Example
241///
242/// ```
243/// # use tonic_build::manual::Method;
244/// let say_hello_method = Method::builder()
245///     .name("say_hello")
246///     .route_name("SayHello")
247///     // Provide the path to the Request type
248///     .input_type("crate::common::HelloRequest")
249///     // Provide the path to the Response type
250///     .output_type("crate::common::HelloResponse")
251///     // Provide the path to the Codec to use
252///     .codec_path("crate::common::JsonCodec")
253///     .build();
254/// ```
255#[derive(Debug, Default)]
256pub struct MethodBuilder {
257    /// The name of the method in Rust style.
258    name: Option<String>,
259    /// The name of the method as should be used when constructing a route
260    route_name: Option<String>,
261    /// The method comments.
262    comments: Vec<String>,
263    /// The input Rust type.
264    input_type: Option<String>,
265    /// The output Rust type.
266    output_type: Option<String>,
267    /// Identifies if client streams multiple client messages.
268    client_streaming: bool,
269    /// Identifies if server streams multiple server messages.
270    server_streaming: bool,
271    /// Identifies if the method is deprecated.
272    deprecated: bool,
273    /// The path to the codec to use for this method
274    codec_path: Option<String>,
275}
276
277impl MethodBuilder {
278    /// Set the name for this Method.
279    ///
280    /// This value will be used for generating the client functions for calling this Method.
281    ///
282    /// Generally this is formatted in snake_case.
283    pub fn name(mut self, name: impl AsRef<str>) -> Self {
284        self.name = Some(name.as_ref().to_owned());
285        self
286    }
287
288    /// Set the route_name for this Method.
289    ///
290    /// This value will be used as part of the route for calling this method.
291    /// Routes have the form: `/<package_name>.<service_name>/<method_route_name>`
292    ///
293    /// Generally this is formatted in PascalCase.
294    pub fn route_name(mut self, route_name: impl AsRef<str>) -> Self {
295        self.route_name = Some(route_name.as_ref().to_owned());
296        self
297    }
298
299    /// Add a comment string that should be included as a doc comment for this Method.
300    pub fn comment(mut self, comment: impl AsRef<str>) -> Self {
301        self.comments.push(comment.as_ref().to_owned());
302        self
303    }
304
305    /// Set the path to the Rust type that should be use for the Request type of this method.
306    pub fn input_type(mut self, input_type: impl AsRef<str>) -> Self {
307        self.input_type = Some(input_type.as_ref().to_owned());
308        self
309    }
310
311    /// Set the path to the Rust type that should be use for the Response type of this method.
312    pub fn output_type(mut self, output_type: impl AsRef<str>) -> Self {
313        self.output_type = Some(output_type.as_ref().to_owned());
314        self
315    }
316
317    /// Set the path to the Rust type that should be used as the `Codec` for this method.
318    ///
319    /// Currently the codegen assumes that this type implements `Default`.
320    pub fn codec_path(mut self, codec_path: impl AsRef<str>) -> Self {
321        self.codec_path = Some(codec_path.as_ref().to_owned());
322        self
323    }
324
325    /// Sets if the Method request from the client is streamed.
326    pub fn client_streaming(mut self) -> Self {
327        self.client_streaming = true;
328        self
329    }
330
331    /// Sets if the Method response from the server is streamed.
332    pub fn server_streaming(mut self) -> Self {
333        self.server_streaming = true;
334        self
335    }
336
337    /// Build a Method
338    ///
339    /// Panics if `name`, `route_name`, `input_type`, `output_type`, or `codec_path` weren't set.
340    pub fn build(self) -> Method {
341        Method {
342            name: self.name.unwrap(),
343            route_name: self.route_name.unwrap(),
344            comments: self.comments,
345            input_type: self.input_type.unwrap(),
346            output_type: self.output_type.unwrap(),
347            client_streaming: self.client_streaming,
348            server_streaming: self.server_streaming,
349            deprecated: self.deprecated,
350            codec_path: self.codec_path.unwrap(),
351        }
352    }
353}
354
355struct ServiceGenerator {
356    builder: Builder,
357    clients: TokenStream,
358    servers: TokenStream,
359}
360
361impl ServiceGenerator {
362    fn generate(&mut self, service: &Service) {
363        if self.builder.build_server {
364            let server = CodeGenBuilder::new()
365                .emit_package(true)
366                .compile_well_known_types(false)
367                .generate_server(service, "");
368
369            self.servers.extend(server);
370        }
371
372        if self.builder.build_client {
373            let client = CodeGenBuilder::new()
374                .emit_package(true)
375                .compile_well_known_types(false)
376                .build_transport(self.builder.build_transport)
377                .generate_client(service, "");
378
379            self.clients.extend(client);
380        }
381    }
382
383    fn finalize(&mut self, buf: &mut String) {
384        if self.builder.build_client && !self.clients.is_empty() {
385            let clients = &self.clients;
386
387            let client_service = quote::quote! {
388                #clients
389            };
390
391            let ast: syn::File = syn::parse2(client_service).expect("not a valid tokenstream");
392            let code = prettyplease::unparse(&ast);
393            buf.push_str(&code);
394
395            self.clients = TokenStream::default();
396        }
397
398        if self.builder.build_server && !self.servers.is_empty() {
399            let servers = &self.servers;
400
401            let server_service = quote::quote! {
402                #servers
403            };
404
405            let ast: syn::File = syn::parse2(server_service).expect("not a valid tokenstream");
406            let code = prettyplease::unparse(&ast);
407            buf.push_str(&code);
408
409            self.servers = TokenStream::default();
410        }
411    }
412}
413
414/// Service generator builder.
415#[derive(Debug)]
416pub struct Builder {
417    build_server: bool,
418    build_client: bool,
419    build_transport: bool,
420
421    out_dir: Option<PathBuf>,
422}
423
424impl Default for Builder {
425    fn default() -> Self {
426        Self {
427            build_server: true,
428            build_client: true,
429            build_transport: true,
430            out_dir: None,
431        }
432    }
433}
434
435impl Builder {
436    /// Create a new Builder
437    pub fn new() -> Self {
438        Self::default()
439    }
440
441    /// Enable or disable gRPC client code generation.
442    ///
443    /// Defaults to enabling client code generation.
444    pub fn build_client(mut self, enable: bool) -> Self {
445        self.build_client = enable;
446        self
447    }
448
449    /// Enable or disable gRPC server code generation.
450    ///
451    /// Defaults to enabling server code generation.
452    pub fn build_server(mut self, enable: bool) -> Self {
453        self.build_server = enable;
454        self
455    }
456
457    /// Enable or disable generated clients and servers to have built-in tonic
458    /// transport features.
459    ///
460    /// When the `transport` feature is disabled this does nothing.
461    pub fn build_transport(mut self, enable: bool) -> Self {
462        self.build_transport = enable;
463        self
464    }
465
466    /// Set the output directory to generate code to.
467    ///
468    /// Defaults to the `OUT_DIR` environment variable.
469    pub fn out_dir(mut self, out_dir: impl AsRef<Path>) -> Self {
470        self.out_dir = Some(out_dir.as_ref().to_path_buf());
471        self
472    }
473
474    /// Performs code generation for the provided services.
475    ///
476    /// Generated services will be output into the directory specified by `out_dir`
477    /// with files named `<package_name>.<service_name>.rs`.
478    pub fn compile(self, services: &[Service]) {
479        let out_dir = if let Some(out_dir) = self.out_dir.as_ref() {
480            out_dir.clone()
481        } else {
482            PathBuf::from(std::env::var("OUT_DIR").unwrap())
483        };
484
485        let mut generator = ServiceGenerator {
486            builder: self,
487            clients: TokenStream::default(),
488            servers: TokenStream::default(),
489        };
490
491        for service in services {
492            generator.generate(service);
493            let mut output = String::new();
494            generator.finalize(&mut output);
495
496            let out_file = out_dir.join(format!("{}.{}.rs", service.package, service.name));
497            fs::write(out_file, output).unwrap();
498        }
499    }
500}