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