1 //! API to generate `.rs` files.
2 //!
3 //! This API does not require `protoc` command present in `$PATH`.
4 //!
5 //! ```
6 //! extern crate protoc_rust;
7 //!
8 //! fn main() {
9 //!     protobuf_codegen_pure::Codegen::new()
10 //!         .out_dir("src/protos")
11 //!         .inputs(&["protos/a.proto", "protos/b.proto"]),
12 //!         .include("protos")
13 //!         .run()
14 //!         .expect("Codegen failed.");
15 //! }
16 //! ```
17 //!
18 //! It is advisable that `protobuf-codegen-pure` build-dependecy version be the same as
19 //! `protobuf` dependency.
20 //!
21 //! The alternative is to use `protoc-rust` crate.
22 
23 #![deny(missing_docs)]
24 #![deny(intra_doc_link_resolution_failure)]
25 
26 extern crate protobuf;
27 extern crate protobuf_codegen;
28 
29 mod convert;
30 
31 use std::collections::HashMap;
32 use std::error::Error;
33 use std::fmt;
34 use std::fs;
35 use std::io;
36 use std::io::Read;
37 use std::path::Path;
38 use std::path::PathBuf;
39 
40 mod model;
41 mod parser;
42 mod str_lit;
43 
44 pub use protobuf_codegen::Customize;
45 
46 #[cfg(test)]
47 mod test_against_protobuf_protos;
48 
49 /// Invoke pure rust codegen. See [crate docs](crate) for example.
50 // TODO: merge with protoc-rust def
51 #[derive(Debug, Default)]
52 pub struct Codegen {
53     /// --lang_out= param
54     out_dir: PathBuf,
55     /// -I args
56     includes: Vec<PathBuf>,
57     /// List of .proto files to compile
58     inputs: Vec<PathBuf>,
59     /// Customize code generation
60     customize: Customize,
61 }
62 
63 impl Codegen {
64     /// Fresh new codegen object.
new() -> Self65     pub fn new() -> Self {
66         Self::default()
67     }
68 
69     /// Set the output directory for codegen.
out_dir(&mut self, out_dir: impl AsRef<Path>) -> &mut Self70     pub fn out_dir(&mut self, out_dir: impl AsRef<Path>) -> &mut Self {
71         self.out_dir = out_dir.as_ref().to_owned();
72         self
73     }
74 
75     /// Add an include directory.
include(&mut self, include: impl AsRef<Path>) -> &mut Self76     pub fn include(&mut self, include: impl AsRef<Path>) -> &mut Self {
77         self.includes.push(include.as_ref().to_owned());
78         self
79     }
80 
81     /// Add include directories.
includes(&mut self, includes: impl IntoIterator<Item = impl AsRef<Path>>) -> &mut Self82     pub fn includes(&mut self, includes: impl IntoIterator<Item = impl AsRef<Path>>) -> &mut Self {
83         for include in includes {
84             self.include(include);
85         }
86         self
87     }
88 
89     /// Add an input (`.proto` file).
input(&mut self, input: impl AsRef<Path>) -> &mut Self90     pub fn input(&mut self, input: impl AsRef<Path>) -> &mut Self {
91         self.inputs.push(input.as_ref().to_owned());
92         self
93     }
94 
95     /// Add inputs (`.proto` files).
inputs(&mut self, inputs: impl IntoIterator<Item = impl AsRef<Path>>) -> &mut Self96     pub fn inputs(&mut self, inputs: impl IntoIterator<Item = impl AsRef<Path>>) -> &mut Self {
97         for input in inputs {
98             self.input(input);
99         }
100         self
101     }
102 
103     /// Specify generated code [`Customize`] object.
customize(&mut self, customize: Customize) -> &mut Self104     pub fn customize(&mut self, customize: Customize) -> &mut Self {
105         self.customize = customize;
106         self
107     }
108 
109     /// Like `protoc --rust_out=...` but without requiring `protoc` or `protoc-gen-rust`
110     /// commands in `$PATH`.
run(&self) -> io::Result<()>111     pub fn run(&self) -> io::Result<()> {
112         let includes: Vec<&Path> = self.includes.iter().map(|p| p.as_path()).collect();
113         let inputs: Vec<&Path> = self.inputs.iter().map(|p| p.as_path()).collect();
114         let p = parse_and_typecheck(&includes, &inputs)?;
115 
116         protobuf_codegen::gen_and_write(
117             &p.file_descriptors,
118             &p.relative_paths,
119             &self.out_dir,
120             &self.customize,
121         )
122     }
123 }
124 
125 /// Arguments for pure rust codegen invocation.
126 // TODO: merge with protoc-rust def
127 #[derive(Debug, Default)]
128 #[deprecated(since = "2.14", note = "Use Codegen object instead")]
129 pub struct Args<'a> {
130     /// --lang_out= param
131     pub out_dir: &'a str,
132     /// -I args
133     pub includes: &'a [&'a str],
134     /// List of .proto files to compile
135     pub input: &'a [&'a str],
136     /// Customize code generation
137     pub customize: Customize,
138 }
139 
140 /// Convert OS path to protobuf path (with slashes)
141 /// Function is `pub(crate)` for test.
relative_path_to_protobuf_path(path: &Path) -> String142 pub(crate) fn relative_path_to_protobuf_path(path: &Path) -> String {
143     assert!(path.is_relative());
144     let path = path.to_str().expect("not a valid UTF-8 name");
145     if cfg!(windows) {
146         path.replace('\\', "/")
147     } else {
148         path.to_owned()
149     }
150 }
151 
152 #[derive(Clone)]
153 struct FileDescriptorPair {
154     parsed: model::FileDescriptor,
155     descriptor: protobuf::descriptor::FileDescriptorProto,
156 }
157 
158 #[derive(Debug)]
159 enum CodegenError {
160     ParserErrorWithLocation(parser::ParserErrorWithLocation),
161     ConvertError(convert::ConvertError),
162 }
163 
164 impl From<parser::ParserErrorWithLocation> for CodegenError {
from(e: parser::ParserErrorWithLocation) -> Self165     fn from(e: parser::ParserErrorWithLocation) -> Self {
166         CodegenError::ParserErrorWithLocation(e)
167     }
168 }
169 
170 impl From<convert::ConvertError> for CodegenError {
from(e: convert::ConvertError) -> Self171     fn from(e: convert::ConvertError) -> Self {
172         CodegenError::ConvertError(e)
173     }
174 }
175 
176 #[derive(Debug)]
177 struct WithFileError {
178     file: String,
179     error: CodegenError,
180 }
181 
182 impl fmt::Display for WithFileError {
fmt(&self, f: &mut fmt::Formatter) -> fmt::Result183     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
184         write!(f, "WithFileError")
185     }
186 }
187 
188 impl Error for WithFileError {
description(&self) -> &str189     fn description(&self) -> &str {
190         "WithFileError"
191     }
192 }
193 
194 struct Run<'a> {
195     parsed_files: HashMap<String, FileDescriptorPair>,
196     includes: &'a [&'a Path],
197 }
198 
199 impl<'a> Run<'a> {
get_file_and_all_deps_already_parsed( &self, protobuf_path: &str, result: &mut HashMap<String, FileDescriptorPair>, )200     fn get_file_and_all_deps_already_parsed(
201         &self,
202         protobuf_path: &str,
203         result: &mut HashMap<String, FileDescriptorPair>,
204     ) {
205         if let Some(_) = result.get(protobuf_path) {
206             return;
207         }
208 
209         let pair = self
210             .parsed_files
211             .get(protobuf_path)
212             .expect("must be already parsed");
213         result.insert(protobuf_path.to_owned(), pair.clone());
214 
215         self.get_all_deps_already_parsed(&pair.parsed, result);
216     }
217 
get_all_deps_already_parsed( &self, parsed: &model::FileDescriptor, result: &mut HashMap<String, FileDescriptorPair>, )218     fn get_all_deps_already_parsed(
219         &self,
220         parsed: &model::FileDescriptor,
221         result: &mut HashMap<String, FileDescriptorPair>,
222     ) {
223         for import in &parsed.import_paths {
224             self.get_file_and_all_deps_already_parsed(import, result);
225         }
226     }
227 
add_file(&mut self, protobuf_path: &str, fs_path: &Path) -> io::Result<()>228     fn add_file(&mut self, protobuf_path: &str, fs_path: &Path) -> io::Result<()> {
229         if let Some(_) = self.parsed_files.get(protobuf_path) {
230             return Ok(());
231         }
232 
233         let mut content = String::new();
234         fs::File::open(fs_path)?.read_to_string(&mut content)?;
235 
236         let parsed = model::FileDescriptor::parse(content).map_err(|e| {
237             io::Error::new(
238                 io::ErrorKind::Other,
239                 WithFileError {
240                     file: format!("{}", fs_path.display()),
241                     error: e.into(),
242                 },
243             )
244         })?;
245 
246         for import_path in &parsed.import_paths {
247             self.add_imported_file(import_path)?;
248         }
249 
250         let mut this_file_deps = HashMap::new();
251         self.get_all_deps_already_parsed(&parsed, &mut this_file_deps);
252 
253         let this_file_deps: Vec<_> = this_file_deps.into_iter().map(|(_, v)| v.parsed).collect();
254 
255         let descriptor =
256             convert::file_descriptor(protobuf_path.to_owned(), &parsed, &this_file_deps).map_err(
257                 |e| {
258                     io::Error::new(
259                         io::ErrorKind::Other,
260                         WithFileError {
261                             file: format!("{}", fs_path.display()),
262                             error: e.into(),
263                         },
264                     )
265                 },
266             )?;
267 
268         self.parsed_files.insert(
269             protobuf_path.to_owned(),
270             FileDescriptorPair { parsed, descriptor },
271         );
272 
273         Ok(())
274     }
275 
add_imported_file(&mut self, protobuf_path: &str) -> io::Result<()>276     fn add_imported_file(&mut self, protobuf_path: &str) -> io::Result<()> {
277         for include_dir in self.includes {
278             let fs_path = Path::new(include_dir).join(protobuf_path);
279             if fs_path.exists() {
280                 return self.add_file(protobuf_path, &fs_path);
281             }
282         }
283 
284         Err(io::Error::new(
285             io::ErrorKind::Other,
286             format!(
287                 "protobuf path {:?} is not found in import path {:?}",
288                 protobuf_path, self.includes
289             ),
290         ))
291     }
292 
add_fs_file(&mut self, fs_path: &Path) -> io::Result<String>293     fn add_fs_file(&mut self, fs_path: &Path) -> io::Result<String> {
294         let relative_path = self
295             .includes
296             .iter()
297             .filter_map(|include_dir| fs_path.strip_prefix(include_dir).ok())
298             .next();
299 
300         match relative_path {
301             Some(relative_path) => {
302                 let protobuf_path = relative_path_to_protobuf_path(relative_path);
303                 self.add_file(&protobuf_path, fs_path)?;
304                 Ok(protobuf_path)
305             }
306             None => Err(io::Error::new(
307                 io::ErrorKind::Other,
308                 format!(
309                     "file {:?} must reside in include path {:?}",
310                     fs_path, self.includes
311                 ),
312             )),
313         }
314     }
315 }
316 
317 #[doc(hidden)]
318 pub struct ParsedAndTypechecked {
319     pub relative_paths: Vec<String>,
320     pub file_descriptors: Vec<protobuf::descriptor::FileDescriptorProto>,
321 }
322 
323 #[doc(hidden)]
parse_and_typecheck( includes: &[&Path], input: &[&Path], ) -> io::Result<ParsedAndTypechecked>324 pub fn parse_and_typecheck(
325     includes: &[&Path],
326     input: &[&Path],
327 ) -> io::Result<ParsedAndTypechecked> {
328     let mut run = Run {
329         parsed_files: HashMap::new(),
330         includes: includes,
331     };
332 
333     let mut relative_paths = Vec::new();
334 
335     for input in input {
336         relative_paths.push(run.add_fs_file(&Path::new(input))?);
337     }
338 
339     let file_descriptors: Vec<_> = run
340         .parsed_files
341         .into_iter()
342         .map(|(_, v)| v.descriptor)
343         .collect();
344 
345     Ok(ParsedAndTypechecked {
346         relative_paths,
347         file_descriptors,
348     })
349 }
350 
351 /// Like `protoc --rust_out=...` but without requiring `protoc` or `protoc-gen-rust`
352 /// commands in `$PATH`.
353 #[deprecated(since = "2.14", note = "Use Codegen instead")]
354 #[allow(deprecated)]
run(args: Args) -> io::Result<()>355 pub fn run(args: Args) -> io::Result<()> {
356     let includes: Vec<&Path> = args.includes.iter().map(|p| Path::new(p)).collect();
357     let inputs: Vec<&Path> = args.input.iter().map(|p| Path::new(p)).collect();
358     let p = parse_and_typecheck(&includes, &inputs)?;
359 
360     protobuf_codegen::gen_and_write(
361         &p.file_descriptors,
362         &p.relative_paths,
363         &Path::new(&args.out_dir),
364         &args.customize,
365     )
366 }
367 
368 #[cfg(test)]
369 mod test {
370     use super::*;
371 
372     #[cfg(windows)]
373     #[test]
test_relative_path_to_protobuf_path_windows()374     fn test_relative_path_to_protobuf_path_windows() {
375         assert_eq!(
376             "foo/bar.proto",
377             relative_path_to_protobuf_path(&Path::new("foo\\bar.proto"))
378         );
379     }
380 
381     #[test]
test_relative_path_to_protobuf_path()382     fn test_relative_path_to_protobuf_path() {
383         assert_eq!(
384             "foo/bar.proto",
385             relative_path_to_protobuf_path(&Path::new("foo/bar.proto"))
386         );
387     }
388 }
389