xref: /tonic/tonic-build/src/lib.rs (revision 8ee85fc4)
1 //! `tonic-build` compiles `proto` files via `prost` and generates service stubs
2 //! and proto definitions for use with `tonic`.
3 //!
4 //! # Feature flags
5 //!
6 //! - `cleanup-markdown`: Enables cleaning up documentation from the generated code. Useful
7 //!   when documentation of the generated code fails `cargo test --doc` for example. The
8 //!   `prost` feature must be enabled to use this feature.
9 //! - `prost`: Enables usage of prost generator (enabled by default).
10 //! - `transport`: Enables generation of `connect` method using `tonic::transport::Channel`
11 //!   (enabled by default).
12 //!
13 //! # Required dependencies
14 //!
15 //! ```toml
16 //! [dependencies]
17 //! tonic = <tonic-version>
18 //! prost = <prost-version>
19 //!
20 //! [build-dependencies]
21 //! tonic-build = <tonic-version>
22 //! ```
23 //!
24 //! # Examples
25 //! Simple
26 //!
27 //! ```rust,no_run
28 //! fn main() -> Result<(), Box<dyn std::error::Error>> {
29 //!     tonic_build::compile_protos("proto/service.proto")?;
30 //!     Ok(())
31 //! }
32 //! ```
33 //!
34 //! Configuration
35 //!
36 //! ```rust,no_run
37 //! fn main() -> Result<(), Box<dyn std::error::Error>> {
38 //!    tonic_build::configure()
39 //!         .build_server(false)
40 //!         .compile_protos(
41 //!             &["proto/helloworld/helloworld.proto"],
42 //!             &["proto/helloworld"],
43 //!         )?;
44 //!    Ok(())
45 //! }
46 //!```
47 //!
48 //! ## NixOS related hints
49 //!
50 //! On NixOS, it is better to specify the location of `PROTOC` and `PROTOC_INCLUDE` explicitly.
51 //!
52 //! ```bash
53 //! $ export PROTOBUF_LOCATION=$(nix-env -q protobuf --out-path --no-name)
54 //! $ export PROTOC=$PROTOBUF_LOCATION/bin/protoc
55 //! $ export PROTOC_INCLUDE=$PROTOBUF_LOCATION/include
56 //! $ cargo build
57 //! ```
58 //!
59 //! The reason being that if `prost_build::compile_protos` fails to generate the resultant package,
60 //! the failure is not obvious until the `include!(concat!(env!("OUT_DIR"), "/resultant.rs"));`
61 //! fails with `No such file or directory` error.
62 
63 #![recursion_limit = "256"]
64 #![doc(
65     html_logo_url = "https://raw.githubusercontent.com/tokio-rs/website/master/public/img/icons/tonic.svg"
66 )]
67 #![doc(issue_tracker_base_url = "https://github.com/hyperium/tonic/issues/")]
68 #![doc(test(no_crate_inject, attr(deny(rust_2018_idioms))))]
69 #![cfg_attr(docsrs, feature(doc_auto_cfg))]
70 
71 use proc_macro2::{Delimiter, Group, Ident, Literal, Punct, Spacing, Span, TokenStream};
72 use quote::TokenStreamExt;
73 
74 /// Prost generator
75 #[cfg(feature = "prost")]
76 mod prost;
77 #[cfg(feature = "prost")]
78 pub use prost_build::Config;
79 #[cfg(feature = "prost")]
80 pub use prost_types::FileDescriptorSet;
81 
82 #[cfg(feature = "prost")]
83 pub use prost::{compile_fds, compile_protos, configure, Builder};
84 
85 pub mod manual;
86 
87 /// Service code generation for client
88 pub mod client;
89 /// Service code generation for Server
90 pub mod server;
91 
92 mod code_gen;
93 pub use code_gen::CodeGenBuilder;
94 
95 mod compile_settings;
96 
97 /// Service generation trait.
98 ///
99 /// This trait can be implemented and consumed
100 /// by `client::generate` and `server::generate`
101 /// to allow any codegen module to generate service
102 /// abstractions.
103 pub trait Service {
104     /// Comment type.
105     type Comment: AsRef<str>;
106 
107     /// Method type.
108     type Method: Method;
109 
110     /// Name of service.
name(&self) -> &str111     fn name(&self) -> &str;
112     /// Package name of service.
package(&self) -> &str113     fn package(&self) -> &str;
114     /// Identifier used to generate type name.
identifier(&self) -> &str115     fn identifier(&self) -> &str;
116     /// Methods provided by service.
methods(&self) -> &[Self::Method]117     fn methods(&self) -> &[Self::Method];
118     /// Get comments about this item.
comment(&self) -> &[Self::Comment]119     fn comment(&self) -> &[Self::Comment];
120 }
121 
122 /// Method generation trait.
123 ///
124 /// Each service contains a set of generic
125 /// `Methods`'s that will be used by codegen
126 /// to generate abstraction implementations for
127 /// the provided methods.
128 pub trait Method {
129     /// Comment type.
130     type Comment: AsRef<str>;
131 
132     /// Name of method.
name(&self) -> &str133     fn name(&self) -> &str;
134     /// Identifier used to generate type name.
identifier(&self) -> &str135     fn identifier(&self) -> &str;
136     /// Path to the codec.
codec_path(&self) -> &str137     fn codec_path(&self) -> &str;
138     /// Method is streamed by client.
client_streaming(&self) -> bool139     fn client_streaming(&self) -> bool;
140     /// Method is streamed by server.
server_streaming(&self) -> bool141     fn server_streaming(&self) -> bool;
142     /// Get comments about this item.
comment(&self) -> &[Self::Comment]143     fn comment(&self) -> &[Self::Comment];
144     /// Method is deprecated.
deprecated(&self) -> bool145     fn deprecated(&self) -> bool {
146         false
147     }
148     /// Type name of request and response.
request_response_name( &self, proto_path: &str, compile_well_known_types: bool, ) -> (TokenStream, TokenStream)149     fn request_response_name(
150         &self,
151         proto_path: &str,
152         compile_well_known_types: bool,
153     ) -> (TokenStream, TokenStream);
154 }
155 
156 /// Attributes that will be added to `mod` and `struct` items.
157 #[derive(Debug, Default, Clone)]
158 pub struct Attributes {
159     /// `mod` attributes.
160     module: Vec<(String, String)>,
161     /// `struct` attributes.
162     structure: Vec<(String, String)>,
163 }
164 
165 impl Attributes {
for_mod(&self, name: &str) -> Vec<syn::Attribute>166     fn for_mod(&self, name: &str) -> Vec<syn::Attribute> {
167         generate_attributes(name, &self.module)
168     }
169 
for_struct(&self, name: &str) -> Vec<syn::Attribute>170     fn for_struct(&self, name: &str) -> Vec<syn::Attribute> {
171         generate_attributes(name, &self.structure)
172     }
173 
174     /// Add an attribute that will be added to `mod` items matching the given pattern.
175     ///
176     /// # Examples
177     ///
178     /// ```
179     /// # use tonic_build::*;
180     /// let mut attributes = Attributes::default();
181     /// attributes.push_mod("my.proto.package", r#"#[cfg(feature = "server")]"#);
182     /// ```
push_mod(&mut self, pattern: impl Into<String>, attr: impl Into<String>)183     pub fn push_mod(&mut self, pattern: impl Into<String>, attr: impl Into<String>) {
184         self.module.push((pattern.into(), attr.into()));
185     }
186 
187     /// Add an attribute that will be added to `struct` items matching the given pattern.
188     ///
189     /// # Examples
190     ///
191     /// ```
192     /// # use tonic_build::*;
193     /// let mut attributes = Attributes::default();
194     /// attributes.push_struct("EchoService", "#[derive(PartialEq)]");
195     /// ```
push_struct(&mut self, pattern: impl Into<String>, attr: impl Into<String>)196     pub fn push_struct(&mut self, pattern: impl Into<String>, attr: impl Into<String>) {
197         self.structure.push((pattern.into(), attr.into()));
198     }
199 }
200 
format_service_name<T: Service>(service: &T, emit_package: bool) -> String201 fn format_service_name<T: Service>(service: &T, emit_package: bool) -> String {
202     let package = if emit_package { service.package() } else { "" };
203     format!(
204         "{}{}{}",
205         package,
206         if package.is_empty() { "" } else { "." },
207         service.identifier(),
208     )
209 }
210 
format_method_path<T: Service>(service: &T, method: &T::Method, emit_package: bool) -> String211 fn format_method_path<T: Service>(service: &T, method: &T::Method, emit_package: bool) -> String {
212     format!(
213         "/{}/{}",
214         format_service_name(service, emit_package),
215         method.identifier()
216     )
217 }
218 
format_method_name<T: Service>(service: &T, method: &T::Method, emit_package: bool) -> String219 fn format_method_name<T: Service>(service: &T, method: &T::Method, emit_package: bool) -> String {
220     format!(
221         "{}.{}",
222         format_service_name(service, emit_package),
223         method.identifier()
224     )
225 }
226 
227 // Generates attributes given a list of (`pattern`, `attribute`) pairs. If `pattern` matches `name`, `attribute` will be included.
generate_attributes<'a>( name: &str, attrs: impl IntoIterator<Item = &'a (String, String)>, ) -> Vec<syn::Attribute>228 fn generate_attributes<'a>(
229     name: &str,
230     attrs: impl IntoIterator<Item = &'a (String, String)>,
231 ) -> Vec<syn::Attribute> {
232     attrs
233         .into_iter()
234         .filter(|(matcher, _)| match_name(matcher, name))
235         .flat_map(|(_, attr)| {
236             // attributes cannot be parsed directly, so we pretend they're on a struct
237             syn::parse_str::<syn::DeriveInput>(&format!("{}\nstruct fake;", attr))
238                 .unwrap()
239                 .attrs
240         })
241         .collect::<Vec<_>>()
242 }
243 
generate_deprecated() -> TokenStream244 fn generate_deprecated() -> TokenStream {
245     let mut deprecated_stream = TokenStream::new();
246     deprecated_stream.append(Ident::new("deprecated", Span::call_site()));
247 
248     let group = Group::new(Delimiter::Bracket, deprecated_stream);
249 
250     let mut stream = TokenStream::new();
251     stream.append(Punct::new('#', Spacing::Alone));
252     stream.append(group);
253 
254     stream
255 }
256 
257 // Generate a singular line of a doc comment
generate_doc_comment<S: AsRef<str>>(comment: S) -> TokenStream258 fn generate_doc_comment<S: AsRef<str>>(comment: S) -> TokenStream {
259     let comment = comment.as_ref();
260 
261     let comment = if !comment.starts_with(' ') {
262         format!(" {}", comment)
263     } else {
264         comment.to_string()
265     };
266 
267     let mut doc_stream = TokenStream::new();
268 
269     doc_stream.append(Ident::new("doc", Span::call_site()));
270     doc_stream.append(Punct::new('=', Spacing::Alone));
271     doc_stream.append(Literal::string(comment.as_ref()));
272 
273     let group = Group::new(Delimiter::Bracket, doc_stream);
274 
275     let mut stream = TokenStream::new();
276     stream.append(Punct::new('#', Spacing::Alone));
277     stream.append(group);
278     stream
279 }
280 
281 // Generate a larger doc comment composed of many lines of doc comments
generate_doc_comments<T: AsRef<str>>(comments: &[T]) -> TokenStream282 fn generate_doc_comments<T: AsRef<str>>(comments: &[T]) -> TokenStream {
283     let mut stream = TokenStream::new();
284 
285     for comment in comments {
286         stream.extend(generate_doc_comment(comment));
287     }
288 
289     stream
290 }
291 
292 // Checks whether a path pattern matches a given path.
match_name(pattern: &str, path: &str) -> bool293 pub(crate) fn match_name(pattern: &str, path: &str) -> bool {
294     if pattern.is_empty() {
295         false
296     } else if pattern == "." || pattern == path {
297         true
298     } else {
299         let pattern_segments = pattern.split('.').collect::<Vec<_>>();
300         let path_segments = path.split('.').collect::<Vec<_>>();
301 
302         if &pattern[..1] == "." {
303             // prefix match
304             if pattern_segments.len() > path_segments.len() {
305                 false
306             } else {
307                 pattern_segments[..] == path_segments[..pattern_segments.len()]
308             }
309         // suffix match
310         } else if pattern_segments.len() > path_segments.len() {
311             false
312         } else {
313             pattern_segments[..] == path_segments[path_segments.len() - pattern_segments.len()..]
314         }
315     }
316 }
317 
naive_snake_case(name: &str) -> String318 fn naive_snake_case(name: &str) -> String {
319     let mut s = String::new();
320     let mut it = name.chars().peekable();
321 
322     while let Some(x) = it.next() {
323         s.push(x.to_ascii_lowercase());
324         if let Some(y) = it.peek() {
325             if y.is_uppercase() {
326                 s.push('_');
327             }
328         }
329     }
330 
331     s
332 }
333 
334 #[cfg(test)]
335 mod tests {
336     use super::*;
337 
338     #[test]
test_match_name()339     fn test_match_name() {
340         assert!(match_name(".", ".my.protos"));
341         assert!(match_name(".", ".protos"));
342 
343         assert!(match_name(".my", ".my"));
344         assert!(match_name(".my", ".my.protos"));
345         assert!(match_name(".my.protos.Service", ".my.protos.Service"));
346 
347         assert!(match_name("Service", ".my.protos.Service"));
348 
349         assert!(!match_name(".m", ".my.protos"));
350         assert!(!match_name(".p", ".protos"));
351 
352         assert!(!match_name(".my", ".myy"));
353         assert!(!match_name(".protos", ".my.protos"));
354         assert!(!match_name(".Service", ".my.protos.Service"));
355 
356         assert!(!match_name("service", ".my.protos.Service"));
357     }
358 
359     #[test]
test_snake_case()360     fn test_snake_case() {
361         for case in &[
362             ("Service", "service"),
363             ("ThatHasALongName", "that_has_a_long_name"),
364             ("greeter", "greeter"),
365             ("ABCServiceX", "a_b_c_service_x"),
366         ] {
367             assert_eq!(naive_snake_case(case.0), case.1)
368         }
369     }
370 }
371