xref: /tonic/tonic-build/src/lib.rs (revision ea7fe66b)
1 //! `tonic-build` compiles `proto` files via `prost` and generates service stubs
2 //! and proto definitiones for use with `tonic`.
3 //!
4 //! # Features
5 //!
6 //! - `rustfmt`: This feature enables the use of `rustfmt` to format the output code
7 //! this makes the code readable and the error messages nice. This requires that `rustfmt`
8 //! is installed. This is enabled by default.
9 //!
10 //! # Required dependencies
11 //!
12 //! ```toml
13 //! [dependencies]
14 //! tonic = <tonic-version>
15 //! prost = <prost-version>
16 //!
17 //! [build-dependencies]
18 //! tonic-build = <tonic-version>
19 //! ```
20 //!
21 //! # Examples
22 //! Simple
23 //!
24 //! ```rust,no_run
25 //! fn main() -> Result<(), Box<dyn std::error::Error>> {
26 //!     tonic_build::compile_protos("proto/service.proto")?;
27 //!     Ok(())
28 //! }
29 //! ```
30 //!
31 //! Configuration
32 //!
33 //! ```rust,no_run
34 //! fn main() -> Result<(), Box<dyn std::error::Error>> {
35 //!    tonic_build::configure()
36 //!         .build_server(false)
37 //!         .compile(
38 //!             &["proto/helloworld/helloworld.proto"],
39 //!             &["proto/helloworld"],
40 //!         )?;
41 //!    Ok(())
42 //! }
43 //! ```
44 
45 #![recursion_limit = "256"]
46 #![warn(
47     missing_debug_implementations,
48     missing_docs,
49     rust_2018_idioms,
50     unreachable_pub
51 )]
52 #![doc(
53     html_logo_url = "https://github.com/hyperium/tonic/raw/master/.github/assets/tonic-docs.png"
54 )]
55 #![doc(html_root_url = "https://docs.rs/tonic-build/0.2.0")]
56 #![doc(issue_tracker_base_url = "https://github.com/hyperium/tonic/issues/")]
57 #![doc(test(no_crate_inject, attr(deny(rust_2018_idioms))))]
58 #![cfg_attr(docsrs, feature(doc_cfg))]
59 
60 use proc_macro2::{Delimiter, Group, Ident, Literal, Punct, Spacing, Span, TokenStream};
61 use quote::TokenStreamExt;
62 
63 /// Prost generator
64 #[cfg(feature = "prost")]
65 #[cfg_attr(docsrs, doc(cfg(feature = "prost")))]
66 mod prost;
67 
68 #[cfg(feature = "prost")]
69 #[cfg_attr(docsrs, doc(cfg(feature = "prost")))]
70 pub use prost::{compile_protos, configure, Builder};
71 
72 #[cfg(feature = "rustfmt")]
73 #[cfg_attr(docsrs, doc(cfg(feature = "rustfmt")))]
74 use std::io::{self, Write};
75 #[cfg(feature = "rustfmt")]
76 #[cfg_attr(docsrs, doc(cfg(feature = "rustfmt")))]
77 use std::process::{exit, Command};
78 
79 /// Service code generation for client
80 pub mod client;
81 /// Service code generation for Server
82 pub mod server;
83 
84 /// Service generation trait.
85 ///
86 /// This trait can be implemented and consumed
87 /// by `client::generate` and `server::generate`
88 /// to allow any codegen module to generate service
89 /// abstractions.
90 pub trait Service {
91     /// Path to the codec.
92     const CODEC_PATH: &'static str;
93 
94     /// Comment type.
95     type Comment: AsRef<str>;
96 
97     /// Method type.
98     type Method: Method;
99 
100     /// Name of service.
101     fn name(&self) -> &str;
102     /// Package name of service.
103     fn package(&self) -> &str;
104     /// Identifier used to generate type name.
105     fn identifier(&self) -> &str;
106     /// Methods provided by service.
107     fn methods(&self) -> &[Self::Method];
108     /// Get comments about this item.
109     fn comment(&self) -> &[Self::Comment];
110 }
111 
112 /// Method generation trait.
113 ///
114 /// Each service contains a set of generic
115 /// `Methods`'s that will be used by codegen
116 /// to generate abstraction implementations for
117 /// the provided methods.
118 pub trait Method {
119     /// Path to the codec.
120     const CODEC_PATH: &'static str;
121     /// Comment type.
122     type Comment: AsRef<str>;
123 
124     /// Name of method.
125     fn name(&self) -> &str;
126     /// Identifier used to generate type name.
127     fn identifier(&self) -> &str;
128     /// Method is streamed by client.
129     fn client_streaming(&self) -> bool;
130     /// Method is streamed by server.
131     fn server_streaming(&self) -> bool;
132     /// Get comments about this item.
133     fn comment(&self) -> &[Self::Comment];
134     /// Type name of request and response.
135     fn request_response_name(&self, proto_path: &str) -> (TokenStream, TokenStream);
136 }
137 
138 /// Format files under the out_dir with rustfmt
139 #[cfg(feature = "rustfmt")]
140 #[cfg_attr(docsrs, doc(cfg(feature = "rustfmt")))]
141 pub fn fmt(out_dir: &str) {
142     let dir = std::fs::read_dir(out_dir).unwrap();
143 
144     for entry in dir {
145         let file = entry.unwrap().file_name().into_string().unwrap();
146         if !file.ends_with(".rs") {
147             continue;
148         }
149         let result = Command::new("rustfmt")
150             .arg("--emit")
151             .arg("files")
152             .arg("--edition")
153             .arg("2018")
154             .arg(format!("{}/{}", out_dir, file))
155             .output();
156 
157         match result {
158             Err(e) => {
159                 eprintln!("error running rustfmt: {:?}", e);
160                 exit(1)
161             }
162             Ok(output) => {
163                 if !output.status.success() {
164                     io::stderr().write_all(&output.stderr).unwrap();
165                     exit(output.status.code().unwrap_or(1))
166                 }
167             }
168         }
169     }
170 }
171 
172 // Generate a singular line of a doc comment
173 fn generate_doc_comment<S: AsRef<str>>(comment: S) -> TokenStream {
174     let mut doc_stream = TokenStream::new();
175 
176     doc_stream.append(Ident::new("doc", Span::call_site()));
177     doc_stream.append(Punct::new('=', Spacing::Alone));
178     doc_stream.append(Literal::string(comment.as_ref()));
179 
180     let group = Group::new(Delimiter::Bracket, doc_stream);
181 
182     let mut stream = TokenStream::new();
183     stream.append(Punct::new('#', Spacing::Alone));
184     stream.append(group);
185     stream
186 }
187 
188 // Generate a larger doc comment composed of many lines of doc comments
189 fn generate_doc_comments<T: AsRef<str>>(comments: &[T]) -> TokenStream {
190     let mut stream = TokenStream::new();
191 
192     for comment in comments {
193         stream.extend(generate_doc_comment(comment));
194     }
195 
196     stream
197 }
198 
199 fn naive_snake_case(name: &str) -> String {
200     let mut s = String::new();
201     let mut it = name.chars().peekable();
202 
203     while let Some(x) = it.next() {
204         s.push(x.to_ascii_lowercase());
205         if let Some(y) = it.peek() {
206             if y.is_uppercase() {
207                 s.push('_');
208             }
209         }
210     }
211 
212     s
213 }
214 
215 #[test]
216 fn test_snake_case() {
217     for case in &[
218         ("Service", "service"),
219         ("ThatHasALongName", "that_has_a_long_name"),
220         ("greeter", "greeter"),
221         ("ABCServiceX", "a_b_c_service_x"),
222     ] {
223         assert_eq!(naive_snake_case(case.0), case.1)
224     }
225 }
226