246 lines
6.9 KiB
Rust
246 lines
6.9 KiB
Rust
//! `tonic-build` compiles `proto` files via `prost` and generates service stubs
|
|
//! and proto definitiones for use with `tonic`.
|
|
//!
|
|
//! # Features
|
|
//!
|
|
//! - `rustfmt`: This feature enables the use of `rustfmt` to format the output code
|
|
//! this makes the code readable and the error messages nice. This requires that `rustfmt`
|
|
//! is installed. This is enabled by default.
|
|
//!
|
|
//! # Required dependencies
|
|
//!
|
|
//! ```toml
|
|
//! [dependencies]
|
|
//! tonic = <tonic-version>
|
|
//! prost = <prost-version>
|
|
//!
|
|
//! [build-dependencies]
|
|
//! tonic-build = <tonic-version>
|
|
//! ```
|
|
//!
|
|
//! # Examples
|
|
//! Simple
|
|
//!
|
|
//! ```rust,no_run
|
|
//! fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
//! tonic_build::compile_protos("proto/service.proto")?;
|
|
//! Ok(())
|
|
//! }
|
|
//! ```
|
|
//!
|
|
//! Configuration
|
|
//!
|
|
//! ```rust,no_run
|
|
//! fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
//! tonic_build::configure()
|
|
//! .build_server(false)
|
|
//! .compile(
|
|
//! &["proto/helloworld/helloworld.proto"],
|
|
//! &["proto/helloworld"],
|
|
//! )?;
|
|
//! Ok(())
|
|
//! }
|
|
//!```
|
|
//!
|
|
//! ## NixOS related hints
|
|
//!
|
|
//! On NixOS, it is better to specify the location of `PROTOC` and `PROTOC_INCLUDE` explicitly.
|
|
//!
|
|
//! ```bash
|
|
//! $ export PROTOBUF_LOCATION=$(nix-env -q protobuf --out-path --no-name)
|
|
//! $ export PROTOC=$PROTOBUF_LOCATION/bin/protoc
|
|
//! $ export PROTOC_INCLUDE=$PROTOBUF_LOCATION/include
|
|
//! $ cargo build
|
|
//! ```
|
|
//!
|
|
//! The reason being that if `prost_build::compile_protos` fails to generate the resultant package,
|
|
//! the failure is not obvious until the `include!(concat!(env!("OUT_DIR"), "/resultant.rs"));`
|
|
//! fails with `No such file or directory` error.
|
|
|
|
#![recursion_limit = "256"]
|
|
#![warn(
|
|
missing_debug_implementations,
|
|
missing_docs,
|
|
rust_2018_idioms,
|
|
unreachable_pub
|
|
)]
|
|
#![doc(
|
|
html_logo_url = "https://raw.githubusercontent.com/tokio-rs/website/master/public/img/icons/tonic.svg"
|
|
)]
|
|
#![doc(html_root_url = "https://docs.rs/tonic-build/0.4.0")]
|
|
#![doc(issue_tracker_base_url = "https://github.com/hyperium/tonic/issues/")]
|
|
#![doc(test(no_crate_inject, attr(deny(rust_2018_idioms))))]
|
|
#![cfg_attr(docsrs, feature(doc_cfg))]
|
|
|
|
use proc_macro2::{Delimiter, Group, Ident, Literal, Punct, Spacing, Span, TokenStream};
|
|
use quote::TokenStreamExt;
|
|
|
|
/// Prost generator
|
|
#[cfg(feature = "prost")]
|
|
#[cfg_attr(docsrs, doc(cfg(feature = "prost")))]
|
|
mod prost;
|
|
|
|
#[cfg(feature = "prost")]
|
|
#[cfg_attr(docsrs, doc(cfg(feature = "prost")))]
|
|
pub use prost::{compile_protos, configure, Builder};
|
|
|
|
#[cfg(feature = "rustfmt")]
|
|
#[cfg_attr(docsrs, doc(cfg(feature = "rustfmt")))]
|
|
use std::io::{self, Write};
|
|
#[cfg(feature = "rustfmt")]
|
|
#[cfg_attr(docsrs, doc(cfg(feature = "rustfmt")))]
|
|
use std::process::{exit, Command};
|
|
|
|
/// Service code generation for client
|
|
pub mod client;
|
|
/// Service code generation for Server
|
|
pub mod server;
|
|
|
|
/// Service generation trait.
|
|
///
|
|
/// This trait can be implemented and consumed
|
|
/// by `client::generate` and `server::generate`
|
|
/// to allow any codegen module to generate service
|
|
/// abstractions.
|
|
pub trait Service {
|
|
/// Path to the codec.
|
|
const CODEC_PATH: &'static str;
|
|
|
|
/// Comment type.
|
|
type Comment: AsRef<str>;
|
|
|
|
/// Method type.
|
|
type Method: Method;
|
|
|
|
/// Name of service.
|
|
fn name(&self) -> &str;
|
|
/// Package name of service.
|
|
fn package(&self) -> &str;
|
|
/// Identifier used to generate type name.
|
|
fn identifier(&self) -> &str;
|
|
/// Methods provided by service.
|
|
fn methods(&self) -> &[Self::Method];
|
|
/// Get comments about this item.
|
|
fn comment(&self) -> &[Self::Comment];
|
|
}
|
|
|
|
/// Method generation trait.
|
|
///
|
|
/// Each service contains a set of generic
|
|
/// `Methods`'s that will be used by codegen
|
|
/// to generate abstraction implementations for
|
|
/// the provided methods.
|
|
pub trait Method {
|
|
/// Path to the codec.
|
|
const CODEC_PATH: &'static str;
|
|
/// Comment type.
|
|
type Comment: AsRef<str>;
|
|
|
|
/// Name of method.
|
|
fn name(&self) -> &str;
|
|
/// Identifier used to generate type name.
|
|
fn identifier(&self) -> &str;
|
|
/// Method is streamed by client.
|
|
fn client_streaming(&self) -> bool;
|
|
/// Method is streamed by server.
|
|
fn server_streaming(&self) -> bool;
|
|
/// Get comments about this item.
|
|
fn comment(&self) -> &[Self::Comment];
|
|
/// Type name of request and response.
|
|
fn request_response_name(
|
|
&self,
|
|
proto_path: &str,
|
|
compile_well_known_types: bool,
|
|
) -> (TokenStream, TokenStream);
|
|
}
|
|
|
|
/// Format files under the out_dir with rustfmt
|
|
#[cfg(feature = "rustfmt")]
|
|
#[cfg_attr(docsrs, doc(cfg(feature = "rustfmt")))]
|
|
pub fn fmt(out_dir: &str) {
|
|
let dir = std::fs::read_dir(out_dir).unwrap();
|
|
|
|
for entry in dir {
|
|
let file = entry.unwrap().file_name().into_string().unwrap();
|
|
if !file.ends_with(".rs") {
|
|
continue;
|
|
}
|
|
let result =
|
|
Command::new(std::env::var("RUSTFMT").unwrap_or_else(|_| "rustfmt".to_owned()))
|
|
.arg("--emit")
|
|
.arg("files")
|
|
.arg("--edition")
|
|
.arg("2018")
|
|
.arg(format!("{}/{}", out_dir, file))
|
|
.output();
|
|
|
|
match result {
|
|
Err(e) => {
|
|
eprintln!("error running rustfmt: {:?}", e);
|
|
exit(1)
|
|
}
|
|
Ok(output) => {
|
|
if !output.status.success() {
|
|
io::stderr().write_all(&output.stderr).unwrap();
|
|
exit(output.status.code().unwrap_or(1))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Generate a singular line of a doc comment
|
|
fn generate_doc_comment<S: AsRef<str>>(comment: S) -> TokenStream {
|
|
let mut doc_stream = TokenStream::new();
|
|
|
|
doc_stream.append(Ident::new("doc", Span::call_site()));
|
|
doc_stream.append(Punct::new('=', Spacing::Alone));
|
|
doc_stream.append(Literal::string(comment.as_ref()));
|
|
|
|
let group = Group::new(Delimiter::Bracket, doc_stream);
|
|
|
|
let mut stream = TokenStream::new();
|
|
stream.append(Punct::new('#', Spacing::Alone));
|
|
stream.append(group);
|
|
stream
|
|
}
|
|
|
|
// Generate a larger doc comment composed of many lines of doc comments
|
|
fn generate_doc_comments<T: AsRef<str>>(comments: &[T]) -> TokenStream {
|
|
let mut stream = TokenStream::new();
|
|
|
|
for comment in comments {
|
|
stream.extend(generate_doc_comment(comment));
|
|
}
|
|
|
|
stream
|
|
}
|
|
|
|
fn naive_snake_case(name: &str) -> String {
|
|
let mut s = String::new();
|
|
let mut it = name.chars().peekable();
|
|
|
|
while let Some(x) = it.next() {
|
|
s.push(x.to_ascii_lowercase());
|
|
if let Some(y) = it.peek() {
|
|
if y.is_uppercase() {
|
|
s.push('_');
|
|
}
|
|
}
|
|
}
|
|
|
|
s
|
|
}
|
|
|
|
#[test]
|
|
fn test_snake_case() {
|
|
for case in &[
|
|
("Service", "service"),
|
|
("ThatHasALongName", "that_has_a_long_name"),
|
|
("greeter", "greeter"),
|
|
("ABCServiceX", "a_b_c_service_x"),
|
|
] {
|
|
assert_eq!(naive_snake_case(case.0), case.1)
|
|
}
|
|
}
|