ttrpc_codegen/
lib.rs

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