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}