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