xref: /tonic/tonic-build/src/lib.rs (revision 209fcbe5)
1 //! `tonic-build` compiles `proto` files via `prost` and generates service stubs
2 //! and proto definitiones 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.10.2")]
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 /// Service generation trait.
101 ///
102 /// This trait can be implemented and consumed
103 /// by `client::generate` and `server::generate`
104 /// to allow any codegen module to generate service
105 /// abstractions.
106 pub trait Service {
107     /// Comment type.
108     type Comment: AsRef<str>;
109 
110     /// Method type.
111     type Method: Method;
112 
113     /// Name of service.
114     fn name(&self) -> &str;
115     /// Package name of service.
116     fn package(&self) -> &str;
117     /// Identifier used to generate type name.
118     fn identifier(&self) -> &str;
119     /// Methods provided by service.
120     fn methods(&self) -> &[Self::Method];
121     /// Get comments about this item.
122     fn comment(&self) -> &[Self::Comment];
123 }
124 
125 /// Method generation trait.
126 ///
127 /// Each service contains a set of generic
128 /// `Methods`'s that will be used by codegen
129 /// to generate abstraction implementations for
130 /// the provided methods.
131 pub trait Method {
132     /// Comment type.
133     type Comment: AsRef<str>;
134 
135     /// Name of method.
136     fn name(&self) -> &str;
137     /// Identifier used to generate type name.
138     fn identifier(&self) -> &str;
139     /// Path to the codec.
140     fn codec_path(&self) -> &str;
141     /// Method is streamed by client.
142     fn client_streaming(&self) -> bool;
143     /// Method is streamed by server.
144     fn server_streaming(&self) -> bool;
145     /// Get comments about this item.
146     fn comment(&self) -> &[Self::Comment];
147     /// Type name of request and response.
148     fn request_response_name(
149         &self,
150         proto_path: &str,
151         compile_well_known_types: bool,
152     ) -> (TokenStream, TokenStream);
153 }
154 
155 /// Attributes that will be added to `mod` and `struct` items.
156 #[derive(Debug, Default, Clone)]
157 pub struct Attributes {
158     /// `mod` attributes.
159     module: Vec<(String, String)>,
160     /// `struct` attributes.
161     structure: Vec<(String, String)>,
162 }
163 
164 impl Attributes {
165     fn for_mod(&self, name: &str) -> Vec<syn::Attribute> {
166         generate_attributes(name, &self.module)
167     }
168 
169     fn for_struct(&self, name: &str) -> Vec<syn::Attribute> {
170         generate_attributes(name, &self.structure)
171     }
172 
173     /// Add an attribute that will be added to `mod` items matching the given pattern.
174     ///
175     /// # Examples
176     ///
177     /// ```
178     /// # use tonic_build::*;
179     /// let mut attributes = Attributes::default();
180     /// attributes.push_mod("my.proto.package", r#"#[cfg(feature = "server")]"#);
181     /// ```
182     pub fn push_mod(&mut self, pattern: impl Into<String>, attr: impl Into<String>) {
183         self.module.push((pattern.into(), attr.into()));
184     }
185 
186     /// Add an attribute that will be added to `struct` items matching the given pattern.
187     ///
188     /// # Examples
189     ///
190     /// ```
191     /// # use tonic_build::*;
192     /// let mut attributes = Attributes::default();
193     /// attributes.push_struct("EchoService", "#[derive(PartialEq)]");
194     /// ```
195     pub fn push_struct(&mut self, pattern: impl Into<String>, attr: impl Into<String>) {
196         self.structure.push((pattern.into(), attr.into()));
197     }
198 }
199 
200 fn format_service_name<T: Service>(service: &T, emit_package: bool) -> String {
201     let package = if emit_package { service.package() } else { "" };
202     format!(
203         "{}{}{}",
204         package,
205         if package.is_empty() { "" } else { "." },
206         service.identifier(),
207     )
208 }
209 
210 fn format_method_path<T: Service>(service: &T, method: &T::Method, emit_package: bool) -> String {
211     format!(
212         "/{}/{}",
213         format_service_name(service, emit_package),
214         method.identifier()
215     )
216 }
217 
218 fn format_method_name<T: Service>(service: &T, method: &T::Method, emit_package: bool) -> String {
219     format!(
220         "{}.{}",
221         format_service_name(service, emit_package),
222         method.identifier()
223     )
224 }
225 
226 // Generates attributes given a list of (`pattern`, `attribute`) pairs. If `pattern` matches `name`, `attribute` will be included.
227 fn generate_attributes<'a>(
228     name: &str,
229     attrs: impl IntoIterator<Item = &'a (String, String)>,
230 ) -> Vec<syn::Attribute> {
231     attrs
232         .into_iter()
233         .filter(|(matcher, _)| match_name(matcher, name))
234         .flat_map(|(_, attr)| {
235             // attributes cannot be parsed directly, so we pretend they're on a struct
236             syn::parse_str::<syn::DeriveInput>(&format!("{}\nstruct fake;", attr))
237                 .unwrap()
238                 .attrs
239         })
240         .collect::<Vec<_>>()
241 }
242 
243 // Generate a singular line of a doc comment
244 fn generate_doc_comment<S: AsRef<str>>(comment: S) -> TokenStream {
245     let comment = comment.as_ref();
246 
247     let comment = if !comment.starts_with(' ') {
248         format!(" {}", comment)
249     } else {
250         comment.to_string()
251     };
252 
253     let mut doc_stream = TokenStream::new();
254 
255     doc_stream.append(Ident::new("doc", Span::call_site()));
256     doc_stream.append(Punct::new('=', Spacing::Alone));
257     doc_stream.append(Literal::string(comment.as_ref()));
258 
259     let group = Group::new(Delimiter::Bracket, doc_stream);
260 
261     let mut stream = TokenStream::new();
262     stream.append(Punct::new('#', Spacing::Alone));
263     stream.append(group);
264     stream
265 }
266 
267 // Generate a larger doc comment composed of many lines of doc comments
268 fn generate_doc_comments<T: AsRef<str>>(comments: &[T]) -> TokenStream {
269     let mut stream = TokenStream::new();
270 
271     for comment in comments {
272         stream.extend(generate_doc_comment(comment));
273     }
274 
275     stream
276 }
277 
278 // Checks whether a path pattern matches a given path.
279 pub(crate) fn match_name(pattern: &str, path: &str) -> bool {
280     if pattern.is_empty() {
281         false
282     } else if pattern == "." || pattern == path {
283         true
284     } else {
285         let pattern_segments = pattern.split('.').collect::<Vec<_>>();
286         let path_segments = path.split('.').collect::<Vec<_>>();
287 
288         if &pattern[..1] == "." {
289             // prefix match
290             if pattern_segments.len() > path_segments.len() {
291                 false
292             } else {
293                 pattern_segments[..] == path_segments[..pattern_segments.len()]
294             }
295         // suffix match
296         } else if pattern_segments.len() > path_segments.len() {
297             false
298         } else {
299             pattern_segments[..] == path_segments[path_segments.len() - pattern_segments.len()..]
300         }
301     }
302 }
303 
304 fn naive_snake_case(name: &str) -> String {
305     let mut s = String::new();
306     let mut it = name.chars().peekable();
307 
308     while let Some(x) = it.next() {
309         s.push(x.to_ascii_lowercase());
310         if let Some(y) = it.peek() {
311             if y.is_uppercase() {
312                 s.push('_');
313             }
314         }
315     }
316 
317     s
318 }
319 
320 #[cfg(test)]
321 mod tests {
322     use super::*;
323 
324     #[test]
325     fn test_match_name() {
326         assert!(match_name(".", ".my.protos"));
327         assert!(match_name(".", ".protos"));
328 
329         assert!(match_name(".my", ".my"));
330         assert!(match_name(".my", ".my.protos"));
331         assert!(match_name(".my.protos.Service", ".my.protos.Service"));
332 
333         assert!(match_name("Service", ".my.protos.Service"));
334 
335         assert!(!match_name(".m", ".my.protos"));
336         assert!(!match_name(".p", ".protos"));
337 
338         assert!(!match_name(".my", ".myy"));
339         assert!(!match_name(".protos", ".my.protos"));
340         assert!(!match_name(".Service", ".my.protos.Service"));
341 
342         assert!(!match_name("service", ".my.protos.Service"));
343     }
344 
345     #[test]
346     fn test_snake_case() {
347         for case in &[
348             ("Service", "service"),
349             ("ThatHasALongName", "that_has_a_long_name"),
350             ("greeter", "greeter"),
351             ("ABCServiceX", "a_b_c_service_x"),
352         ] {
353             assert_eq!(naive_snake_case(case.0), case.1)
354         }
355     }
356 }
357