protoc_grpcio/
lib.rs

1// Copyright 2018. Matthew Pelland <matt@pelland.io>.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// See the License for the specific language governing permissions and
12// limitations under the License.
13//
14// Parts of this work are derived from the `protoc-rust-grpc` crate by
15// Stepan Koltsov <stepan.koltsov@gmail.com>.
16//
17// Copyright 2016, Stepan Koltsov <stepan.koltsov@gmail.com>.
18//
19// Licensed under the Apache License, Version 2.0 (the "License");
20// you may not use this file except in compliance with the License.
21// You may obtain a copy of the License at
22//
23//     http://www.apache.org/licenses/LICENSE-2.0
24//
25// Unless required by applicable law or agreed to in writing, software
26// distributed under the License is distributed on an "AS IS" BASIS,
27// See the License for the specific language governing permissions and
28// limitations under the License.
29#![deny(warnings)]
30#![warn(missing_docs)]
31//! An API for programmatically invoking the grpcio gRPC compiler in the same vein as the
32//! [rust-protoc-grpc](https://crates.io/crates/protoc-rust-grpc) crate from Stepan Koltsov.
33
34extern crate grpcio_compiler;
35
36#[macro_use]
37extern crate failure;
38
39extern crate tempfile;
40
41extern crate protobuf;
42extern crate protobuf_codegen;
43extern crate protoc;
44
45use std::convert::AsRef;
46use std::fs::File;
47use std::io::{Read, Write};
48use std::iter::Iterator;
49use std::path::{Path, PathBuf};
50use std::vec::Vec;
51
52use failure::ResultExt;
53
54use tempfile::NamedTempFile;
55
56use protobuf::{compiler_plugin, descriptor};
57use protobuf_codegen::Customize;
58use protoc::{DescriptorSetOutArgs, Protoc};
59
60/// Custom error type used throughout this crate.
61pub type CompileError = ::failure::Error;
62/// Custom result type used throughout this crate.
63pub type CompileResult<T> = Result<T, CompileError>;
64
65fn stringify_paths<Paths>(paths: Paths) -> CompileResult<Vec<String>>
66where
67    Paths: IntoIterator,
68    Paths::Item: AsRef<Path>,
69{
70    paths
71        .into_iter()
72        .map(|input| match input.as_ref().to_str() {
73            Some(s) => Ok(s.to_owned()),
74            None => Err(format_err!(
75                "failed to convert {:?} to string",
76                input.as_ref()
77            )),
78        })
79        .collect()
80}
81
82fn write_out_generated_files<P>(
83    generation_results: Vec<compiler_plugin::GenResult>,
84    output_dir: P,
85) -> CompileResult<()>
86where
87    P: AsRef<Path>,
88{
89    for result in generation_results {
90        let file = output_dir.as_ref().join(result.name);
91        File::create(&file)
92            .context(format!("failed to create {:?}", &file))?
93            .write_all(&result.content)
94            .context(format!("failed to write {:?}", &file))?;
95    }
96
97    Ok(())
98}
99
100fn absolutize<P>(path: P) -> CompileResult<PathBuf>
101where
102    P: AsRef<Path>,
103{
104    let p = path.as_ref();
105    if p.is_relative() {
106        match std::env::current_dir() {
107            Ok(cwd) => Ok(cwd.join(p)),
108            Err(err) => Err(format_err!(
109                "Failed to determine CWD needed to absolutize a relative path: {:?}",
110                err
111            )),
112        }
113    } else {
114        Ok(PathBuf::from(p))
115    }
116}
117
118fn normalize<Paths, Bases>(
119    paths: Paths,
120    bases: Bases,
121) -> CompileResult<(Vec<PathBuf>, Vec<PathBuf>, Vec<PathBuf>)>
122where
123    Paths: IntoIterator,
124    Paths::Item: AsRef<Path>,
125    Bases: IntoIterator,
126    Bases::Item: AsRef<Path>,
127{
128    let absolutized_bases = bases
129        .into_iter()
130        .map(absolutize)
131        .collect::<CompileResult<Vec<PathBuf>>>()?;
132
133    // We deal with the following cases:
134    // a.) absolute paths
135    // b.) paths relative to CWD
136    // c.) paths relative to bases
137    //
138    // We take the strategy of transforming the relative path cases (b & c) into absolute paths (a)
139    // and use the strip_prefix API from there.
140
141    let absolutized_paths = paths
142        .into_iter()
143        .map(|p| {
144            let rel_path = p.as_ref().to_path_buf();
145            let absolute_path = absolutize(&rel_path)?;
146            Ok((rel_path, absolute_path))
147        })
148        // TODO(John Sirois): Use `.flatten()` pending https://github.com/rust-lang/rust/issues/48213
149        .flat_map(|r: CompileResult<(PathBuf, PathBuf)>| r)
150        .map(|(rel_path, abs_path)| {
151            if abs_path.exists() {
152                // Case a or b.
153                Ok(abs_path)
154            } else {
155                // Case c.
156                for b in &absolutized_bases {
157                    let absolutized_path = b.join(&rel_path);
158                    if absolutized_path.exists() {
159                        return Ok(absolutized_path);
160                    }
161                }
162                Err(format_err!(
163                    "Failed to find the absolute path of input {:?}",
164                    rel_path
165                ))
166            }
167        })
168        .collect::<CompileResult<Vec<PathBuf>>>()?;
169
170    let relativized_paths: Vec<PathBuf> = absolutized_paths
171        .iter()
172        .map(|p| {
173            for b in &absolutized_bases {
174                if let Ok(rel_path) = p.strip_prefix(&b) {
175                    return Ok(PathBuf::from(rel_path));
176                }
177            }
178            Err(format_err!(
179                "The input path {:?} is not contained by any of the include paths {:?}",
180                p,
181                absolutized_bases
182            ))
183        })
184        .collect::<CompileResult<Vec<PathBuf>>>()?;
185
186    Ok((absolutized_bases, absolutized_paths, relativized_paths))
187}
188
189/// Compiles a list a gRPC definitions to rust modules.
190///
191/// # Arguments
192///
193/// * `inputs` - A list of protobuf definition paths to compile. Paths can be specified as absolute,
194///    relative to the CWD or relative to one of the `includes` paths. Note that the directory each
195///    member of `inputs` is found under must be included in the `includes` parameter.
196/// * `includes` - A list of of include directory paths to pass to `protoc`. Include paths can be
197///    specified either as absolute or relative to the CWD. Note that the directory each member of
198///    `inputs` is found under must be included in this parameter.
199/// * `output` - Directory to place the generated rust modules into.
200/// * `customizations` - An Option<protobuf_codegen::Customize> allowing customization options to be
201///    passed to protobuf_codegen
202pub fn compile_grpc_protos<Inputs, Includes, Output>(
203    inputs: Inputs,
204    includes: Includes,
205    output: Output,
206    customizations: Option<Customize>,
207) -> CompileResult<()>
208where
209    Inputs: IntoIterator,
210    Inputs::Item: AsRef<Path>,
211    Includes: IntoIterator,
212    Includes::Item: AsRef<Path>,
213    Output: AsRef<Path>,
214{
215    let protoc = Protoc::from_env_path();
216
217    protoc
218        .check()
219        .context("failed to find `protoc`, `protoc` must be availabe in `PATH`")?;
220
221    let (absolutized_includes, absolutized_paths, relativized_inputs) =
222        normalize(inputs, includes)?;
223    let stringified_inputs_absolute = stringify_paths(absolutized_paths)?;
224    let stringified_inputs = stringify_paths(relativized_inputs)?;
225    let stringified_includes = stringify_paths(absolutized_includes)?;
226
227    let descriptor_set = NamedTempFile::new()?;
228
229    protoc
230        .write_descriptor_set(DescriptorSetOutArgs {
231            out: match descriptor_set.as_ref().to_str() {
232                Some(s) => s,
233                None => bail!("failed to convert descriptor set path to string"),
234            },
235            input: stringified_inputs_absolute
236                .iter()
237                .map(String::as_str)
238                .collect::<Vec<&str>>()
239                .as_slice(),
240            includes: stringified_includes
241                .iter()
242                .map(String::as_str)
243                .collect::<Vec<&str>>()
244                .as_slice(),
245            include_imports: true,
246        })
247        .context("failed to write descriptor set")?;
248
249    let mut serialized_descriptor_set = Vec::new();
250    File::open(&descriptor_set)
251        .context("failed to open descriptor set")?
252        .read_to_end(&mut serialized_descriptor_set)
253        .context("failed to read descriptor set")?;
254
255    let descriptor_set =
256        protobuf::parse_from_bytes::<descriptor::FileDescriptorSet>(&serialized_descriptor_set)
257            .context("failed to parse descriptor set")?;
258
259    let customize = customizations.unwrap_or_default();
260
261    write_out_generated_files(
262        grpcio_compiler::codegen::gen(descriptor_set.get_file(), stringified_inputs.as_slice()),
263        &output,
264    )
265    .context("failed to write generated grpc definitions")?;
266
267    write_out_generated_files(
268        protobuf_codegen::gen(
269            descriptor_set.get_file(),
270            stringified_inputs.as_slice(),
271            &customize,
272        ),
273        &output,
274    )
275    .context("failed to write out generated protobuf definitions")?;
276
277    Ok(())
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283    use std::path::PathBuf;
284    use tempfile::tempdir;
285
286    fn assert_compile_grpc_protos<Input, Output>(input: Input, expected_outputs: Output)
287    where
288        Input: AsRef<Path>,
289        Output: IntoIterator + Copy,
290        Output::Item: AsRef<Path>,
291    {
292        let rel_include_path = PathBuf::from("test/assets/protos");
293        let abs_include_path = Path::new(env!("CARGO_MANIFEST_DIR")).join(&rel_include_path);
294        for include_path in &[&rel_include_path, &abs_include_path] {
295            for inputs in &[vec![input.as_ref()], vec![&include_path.join(&input)]] {
296                let temp_dir = tempdir().unwrap();
297                compile_grpc_protos(inputs, &[include_path], &temp_dir, None).unwrap();
298
299                for output in expected_outputs {
300                    assert!(temp_dir.as_ref().join(output).is_file());
301                }
302            }
303        }
304    }
305
306    #[test]
307    fn test_compile_grpc_protos() {
308        assert_compile_grpc_protos("helloworld.proto", &["helloworld_grpc.rs", "helloworld.rs"])
309    }
310
311    #[test]
312    fn test_compile_grpc_protos_subdir() {
313        assert_compile_grpc_protos("foo/bar/baz.proto", &["baz_grpc.rs", "baz.rs"])
314    }
315}