protoc_plugin_by_closure/
lib.rs

1// Copyright 2021 Google LLC
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// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#![doc = include_str!("../readme.md")]
16
17use ipc_channel::ipc::{IpcBytesReceiver, IpcBytesSender, IpcOneShotServer};
18use std::env;
19use std::path::PathBuf;
20use std::process::{Command, ExitStatus};
21use std::time::Duration;
22#[cfg(feature = "on-memory")]
23use tempfile::TempDir;
24use thiserror::Error;
25use wait_timeout::ChildExt;
26
27const PLUGIN_PATH: &'static str = env!("CARGO_BIN_FILE_PROTOC_PLUGIN_BIN");
28
29/// Error type for this crate.
30#[derive(Error, Debug)]
31pub enum ErrorKind {
32    #[error("IpcIpcError: {0}")]
33    IpcIpcError(#[from] ::ipc_channel::ipc::IpcError),
34    #[error("IpcError: {0}")]
35    IpcError(#[from] ::ipc_channel::Error),
36    #[error("IoError: {0}")]
37    IoError(#[from] ::std::io::Error),
38    #[error("CallbackError: {0}")]
39    CallbackError(String),
40    #[error("ProtocTimeoutError")]
41    ProtocTimeoutError,
42    #[error("ProtocProcessError: {0}")]
43    ProtocProcessError(ExitStatus),
44    #[error("FileNameError")]
45    FileNameError,
46}
47
48/// Result type for this crate.
49pub type Result<T> = ::std::result::Result<T, ErrorKind>;
50
51/// A convenient wrapper for running protoc command with your own plugin code as a closure.
52///
53/// See the [crate level documentation](crate) for the basic explanation.
54///
55/// # Example
56/// ```no_run
57/// # fn run_protoc() {
58///     use protoc_plugin_by_closure::Protoc;
59///     use std::time::Duration;
60///     Protoc::new()
61///         .proto_file("my_protobuf_file.proto")
62///         .proto_file("my_protobuf_file2.proto")
63///         .proto_path("path/to/my/input_proto_dir/")
64///         .out_dir("path/to/my/output_dir/")
65///         .run(Duration::from_sec(3), |request_bytes| {
66///             // Your plugin logic here, which takes the CodeGeneratorRequest bytes
67///             // and returns the Result of CodeGeneratorResponse bytes.
68/// #           unimplemented!()
69///         })
70///         .unwrap();
71///
72///     // The generated file names depend on your plugin logic and the contents of
73///     // the input proto files, but typically they will be like this:
74///     assert!(std::path::Path("path/to/my/output_dir/my_protobuf_file.rs").exists());
75///     assert!(std::path::Path("path/to/my/output_dir/my_protobuf_file2.rs").exists());
76/// # }
77/// ```
78pub struct Protoc {
79    protoc_path: PathBuf,
80    out_dir: Option<PathBuf>,
81    proto_files: Vec<PathBuf>,
82    proto_paths: Vec<PathBuf>,
83}
84
85impl Protoc {
86    /// Creates a new `Protoc` instance.
87    pub fn new() -> Self {
88        Self {
89            protoc_path: "protoc".into(),
90            out_dir: None,
91            proto_files: Vec::new(),
92            proto_paths: Vec::new(),
93        }
94    }
95    /// Sets the path to the `protoc` command. Default is `"protoc"`.
96    pub fn protoc_path(mut self, path: impl Into<PathBuf>) -> Self {
97        self.protoc_path = path.into();
98        self
99    }
100    /// Sets the output directory for the generated files. Corresponds to `--rust_out` option of `protoc`.
101    pub fn out_dir(mut self, path: impl Into<PathBuf>) -> Self {
102        self.out_dir = Some(path.into());
103        self
104    }
105    /// Sets the path to the input proto file. Corresponds to the unnamed argument of `protoc`.
106    pub fn proto_file(mut self, path: impl Into<PathBuf>) -> Self {
107        self.proto_files.push(path.into());
108        self
109    }
110    /// Sets the paths to the input proto files. Corresponds to the unnamed arguments of `protoc`.
111    pub fn proto_files<I>(mut self, paths: I) -> Self
112    where
113        I: IntoIterator,
114        I::Item: Into<PathBuf>,
115    {
116        self.proto_files.extend(paths.into_iter().map(|p| p.into()));
117        self
118    }
119    /// Sets the path to the input proto file directory. Corresponds to `--proto_path` option of `protoc`.
120    pub fn proto_path(mut self, path: impl Into<PathBuf>) -> Self {
121        self.proto_paths.push(path.into());
122        self
123    }
124
125    /// Runs the `protoc` command with the given closure as a plugin code.
126    ///
127    /// The `body` param can be any `FnOnce` closure which takes the encoded `CodeGeneratorRequest` bytes
128    /// and returns the `Result` of encoded `CodeGeneratorResponse` bytes.
129    ///
130    /// Set the `timeout` to the maximum duration of the `protoc` command execution.
131    pub fn run<F>(self, timeout: Duration, body: F) -> Result<()>
132    where
133        F: FnOnce(&[u8]) -> ::std::result::Result<Vec<u8>, String>,
134    {
135        let (ipc_init_server, ipc_init_name) = IpcOneShotServer::new()?;
136
137        let mut process = Command::new(&self.protoc_path)
138            .args(&[
139                // We name our plugin binary name as "rust-ppbc" here.
140                format!("--plugin=protoc-gen-rust-ppbc={}", PLUGIN_PATH),
141                format!(
142                    "--rust-ppbc_out={}",
143                    self.out_dir
144                        .as_ref()
145                        .map(|p| p.to_str().ok_or(ErrorKind::FileNameError))
146                        .transpose()?
147                        .unwrap_or(".")
148                ),
149                format!("--rust-ppbc_opt={}", ipc_init_name),
150            ])
151            .args(
152                self.proto_paths
153                    .iter()
154                    .map(|x| {
155                        Ok(format!(
156                            "--proto_path={}",
157                            x.to_str().ok_or(ErrorKind::FileNameError)?
158                        ))
159                    })
160                    .collect::<Result<Vec<_>>>()?,
161            )
162            .args(&self.proto_files)
163            .spawn()?;
164
165        {
166            // recieve the ipc channels from the plugin exe.
167            let (req_recv, res_send): (IpcBytesReceiver, IpcBytesSender) =
168                ipc_init_server.accept()?.1;
169
170            let req = req_recv.recv()?;
171            let res = (body)(&req).map_err(|x| ErrorKind::CallbackError(x))?;
172
173            res_send.send(&res)?;
174        }
175
176        let Some(exit_code) = process.wait_timeout(timeout)? else {
177            return Err(ErrorKind::ProtocTimeoutError);
178        };
179        if !exit_code.success() {
180            return Err(ErrorKind::ProtocProcessError(exit_code));
181        }
182
183        Ok(())
184    }
185}
186
187/// A variant of [`Protoc`] which you can run the `protoc` command without touching the actual filesystem.
188///
189/// Instead of using the actual filesystem, you can pass the name-value pairs of
190/// proto files to this struct, and it returns the generated files as name-value pairs.
191///
192/// This is useful when you want to test your plugin code, or when you want to implement your
193/// procedual macro which generates the inlined generated code.
194///
195/// See the [crate level documentation](crate) or [`Protoc`] for the basic explanations.
196///
197/// # Example
198/// ```no_run
199/// # fn run_protoc() {
200///     use protoc_plugin_by_closure::ProtocOnMemory;
201///     use std::time::Duration;
202///     let result_files = Protoc::new()
203///         .add_file("my_protobuf_file.proto", r#"
204/// syntax = "proto3";
205/// package my_package;
206/// message MyMessage {
207///   string name = 1;
208/// }"#)
209///         .add_file("another/path/to/my_protobuf_file2.proto", r#"
210/// syntax = "proto3";
211/// package my_package2;
212/// message MyMessage2 {
213///   string name2 = 2;
214/// }"#)
215///         .run(Duration::from_sec(3), |request_bytes| {
216///             // Your plugin logic here, which takes the CodeGeneratorRequest bytes
217///             // and returns the Result of CodeGeneratorResponse bytes.
218/// #           unimplemented!()
219///         })
220///         .unwrap();
221///
222///     // The generated filenames depend on your plugin logic, but typically they will be like this:
223///     assert!(result_files.iter().any(|(name, _)| name == "my_package.rs"));
224///     assert!(result_files.iter().any(|(name, _)| name == "my_package2.rs"));
225/// # }
226/// ```
227#[cfg(feature = "on-memory")]
228pub struct ProtocOnMemory {
229    protoc: Protoc,
230    in_files: Vec<(String, String)>,
231}
232
233impl ProtocOnMemory {
234    /// Creates a new `ProtocOnMemory` instance.
235    pub fn new() -> Self {
236        Self {
237            protoc: Protoc::new(),
238            in_files: Vec::new(),
239        }
240    }
241    /// Sets the path to the `protoc` command. Default is `"protoc"`.
242    pub fn protoc_path(mut self, path: impl Into<PathBuf>) -> Self {
243        self.protoc = self.protoc.protoc_path(path);
244        self
245    }
246    /// Adds a (virtual) input proto file. Corresponds to the `protoc` command's unnamed argument.
247    pub fn add_file(mut self, name: &str, content: &str) -> Self {
248        self.in_files.push((name.to_string(), content.to_string()));
249        self
250    }
251    /// Adds (virtual) input proto files. Corresponds to the `protoc` command's unnamed arguments.
252    pub fn add_files<I>(mut self, files: I) -> Self
253    where
254        I: IntoIterator<Item = (String, String)>,
255    {
256        self.in_files.extend(files);
257        self
258    }
259
260    /// Runs the `protoc` command with the given closure as a plugin code.
261    ///
262    /// The `body` param can be any `FnOnce` closure which takes the encoded `CodeGeneratorRequest` bytes
263    /// and returns the `Result` of encoded `CodeGeneratorResponse` bytes.
264    ///
265    /// Set the `timeout` to the maximum duration of the `protoc` command execution.
266    pub fn run<F>(self, timeout: Duration, func: F) -> Result<Vec<(String, String)>>
267    where
268        F: FnOnce(&[u8]) -> ::std::result::Result<Vec<u8>, String>,
269    {
270        let proto_dir = TempDir::new()?;
271        let out_dir = TempDir::new()?;
272        let out_dir_path = out_dir.path().to_str().unwrap().to_string();
273
274        // write the proto files to the temp dir.
275        for (name, content) in &self.in_files {
276            let path = proto_dir.path().join(&name);
277            ::std::fs::write(&path, &content)?;
278        }
279
280        // run the protoc
281        let proto_file_paths = self
282            .in_files
283            .iter()
284            .map(|(name, _)| {
285                proto_dir
286                    .path()
287                    .join(name)
288                    .to_str()
289                    .ok_or(ErrorKind::FileNameError)
290                    .map(str::to_string)
291            })
292            .collect::<Result<Vec<_>>>()?;
293
294        self.protoc
295            .out_dir(&out_dir_path)
296            .proto_path(proto_dir.path().to_str().unwrap())
297            .proto_files(proto_file_paths)
298            .run(timeout, func)?;
299
300        // read the generated files
301        let output_files = ::std::fs::read_dir(out_dir.path())?
302            .map(|entry| -> Result<_> {
303                let path = entry?.path();
304                let name = path
305                    .strip_prefix(out_dir.path())
306                    .map_err(|_| ErrorKind::FileNameError)?
307                    .to_str()
308                    .ok_or(ErrorKind::FileNameError)?
309                    .to_string();
310                let content = ::std::fs::read_to_string(&path)?;
311                Ok((name, content))
312            })
313            .collect::<Result<Vec<_>>>()?;
314
315        Ok(output_files)
316    }
317}