tonic_build_codec/
lib.rs

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