Skip to main content

rust_grpc_lib/
build_support.rs

1//! Build-time helpers for compiling the bundled `.proto` definitions into Rust.
2//!
3//! Two public items are defined here:
4//!
5//! - [`generate_protos`] — the single function consumers call from their
6//!   `build.rs`. It discovers all `.proto` files shipped with this crate,
7//!   compiles them with `tonic-prost-build`, and writes `proto.rs` into
8//!   `OUT_DIR`.
9//! - [`Config`] — a builder that wraps `tonic_prost_build::Builder` and
10//!   exposes attribute-injection methods for customising the generated code.
11//!
12//! Private helpers live in the [`file_utils`] and [`proto_rs`] sub-modules.
13
14use std::{env, error::Error, ffi::OsStr, fs, path::Path};
15
16mod file_utils;
17mod proto_rs;
18
19/// Compile the bundled `.proto` definitions and write a `proto.rs` into your
20/// crate's `OUT_DIR`.
21///
22/// Call this from your own `build.rs` to generate Rust types for all Controls
23/// gRPC services. The `.proto` files are shipped with this library, so you do
24/// not need to vendor them yourself.
25///
26/// # Setup
27///
28/// Add `rust-grpc-lib` with the `build` feature to your build dependencies:
29///
30/// ```toml
31/// [build-dependencies]
32/// rust-grpc-lib = { git = "https://github.com/fermi-ad/rust-grpc-lib", tag = "vX.Y.Z", features = ["build"] }
33/// ```
34///
35/// Then create a `build.rs` at the root of your crate:
36///
37/// ```rust,ignore
38/// use rust_grpc_lib::build_support::{ Config, generate_protos };
39///
40/// fn main() -> Result<(), Box<dyn std::error::Error>> {
41///     let mut config = Config::new();
42///     // ... configure custom attributes for the generated code here
43///     // e.g., config = config.type_attribute(".some.package", "#[derive(my_custom_attr)]");
44///     generate_protos(config)?;
45///     Ok(())
46/// }
47/// ```
48///
49/// # Using the generated types
50///
51/// The function writes a single file, `proto.rs`, into the directory given by
52/// the `OUT_DIR` environment variable (set automatically by Cargo). Include it
53/// wherever you want the generated module to live:
54///
55/// ```rust,ignore
56/// // src/proto.rs  — or inline in src/lib.rs
57/// include!(concat!(env!("OUT_DIR"), "/proto.rs"));
58/// ```
59///
60/// After that, all generated message types and service clients are accessible
61/// through that module:
62///
63/// ```rust,ignore
64/// mod proto {
65///     include!(concat!(env!("OUT_DIR"), "/proto.rs"));
66/// }
67///
68/// use proto::services::alarm_commands::alarm_commands_client::AlarmCommandsClient;
69/// ```
70///
71/// # Customizing generated attributes
72///
73/// The provided [`Config`] struct exposes functions to add custom attributes to the
74/// generated code.
75///
76/// Example — add `serde` derives to every type in the `common.alarm` package:
77///
78/// ```rust,ignore
79/// let mut config = Config::new();
80/// config = config.type_attribute(".common.alarm", "#[derive(serde::Serialize, serde::Deserialize)]");
81/// ```
82///
83/// # Safety note
84///
85/// This function calls [`std::env::set_var`] to point `PROTOC` at the vendored
86/// `protoc` binary. That call is only safe when no other threads are reading
87/// the environment concurrently. Cargo runs `build.rs` in a single-threaded
88/// process, so this is safe in normal usage. Do **not** call this function from
89/// a multi-threaded context.
90///
91/// # Errors
92///
93/// Returns an error if:
94/// - the bundled `.proto` files cannot be read,
95/// - `tonic-prost-build` fails to compile the protos, or
96/// - `OUT_DIR` is not set (i.e. the function is called outside of a build script)
97pub fn generate_protos(config: Config) -> Result<(), Box<dyn Error>> {
98    // `env!("CARGO_MANIFEST_DIR")` is resolved at compile time of *this* crate
99    // and baked into the binary. Because the `.proto` files are included in the
100    // published crate via the `include` field in Cargo.toml, this path is valid
101    // on the consumer's machine even though it points into this crate's source.
102    let interface_dir = concat!(env!("CARGO_MANIFEST_DIR"), "/interface-definitions");
103    let mut proto_dir = interface_dir.to_string();
104    proto_dir.push_str("/proto");
105    let proto_files = file_utils::find_proto_files(&proto_dir, OsStr::new("proto"))?;
106
107    compile(interface_dir, &proto_files, config)?;
108
109    let packages = file_utils::collect_packages(&proto_files)?;
110    let proto_rs_source = proto_rs::build_contents(&packages);
111
112    let out_dir = env::var("OUT_DIR")?;
113    let out_path = Path::new(&out_dir).join("proto.rs");
114    fs::write(&out_path, proto_rs_source)?;
115
116    Ok(())
117}
118
119/// Provides configuration options for the generated code.
120///
121/// Acquire an instance with the [`new`](Self::new) function, then add any attributes as needed
122/// before passing to [`generate_protos`].
123///
124/// Each attribute function consumes the current instance of [`Config`] and returns an updated one.
125/// Calls may be chained, like so:
126///
127/// ```rust,ignore
128/// let config = Config::new()
129///     .type_attribute("some.package", "#[my_attr]")
130///     .message_attribute("some.package.MyMessage", "#[message_specific_attr]")
131///     .server_mod_attribute("other.package", "#[server_module_attr]");
132/// ```
133///
134/// Alternatively, declare your `config` variable as mutable and replace it with each call:
135///
136/// ```rust,ignore
137/// let mut config = Config::new();
138/// config = config.type_attribute("some.package", "#[my_attr]");
139/// config = config.message_attribute("some.package.MyMessage", "#[message_specific_attr]");
140/// // ...
141/// ```
142pub struct Config {
143    builder: tonic_prost_build::Builder,
144}
145
146impl Config {
147    /// Create a default [`Config`].
148    ///
149    /// Applies the following baseline settings to the underlying
150    /// `tonic_prost_build::Builder`:
151    ///
152    /// - `emit_rerun_if_changed(true)` — Cargo will re-run `build.rs` when
153    ///   any `.proto` file changes.
154    /// - `compile_well_known_types(true)` — generates `google.protobuf.*`
155    ///   types alongside the service types.
156    /// - `#[derive(::rust_grpc_lib::GrpcClient)]` on every client struct —
157    ///   when the `auth` feature is enabled (the default).
158    /// - `#[derive(::rust_grpc_lib::GrpcNoAuthClient)]` on every client
159    ///   struct — when the `unauthenticated` feature is enabled.
160    pub fn new() -> Self {
161        let mut builder = tonic_prost_build::configure()
162            .emit_rerun_if_changed(true)
163            .compile_well_known_types(true);
164
165        if cfg!(any(feature = "auth", test)) {
166            builder = builder.client_attribute(".", "#[derive(::rust_grpc_lib::GrpcClient)]");
167        }
168
169        if cfg!(any(feature = "unauthenticated", test)) {
170            builder = builder.client_attribute(".", "#[derive(::rust_grpc_lib::GrpcNoAuthClient)]");
171        }
172
173        Config { builder }
174    }
175
176    /// Add an additional attribute to generated gRPC client service structs.
177    ///
178    /// # Differentiation from other attribute functions
179    /// - **Scope:** gRPC-specific tooling. This targets the generated implementation client (e.g., `pub struct MyServiceClient<T>`).
180    /// - **Vs `server_attribute`:** Modifies the outgoing client consumer types, leaving the server handler traits untouched.
181    /// - **Vs `type_attribute` / `message_attribute`:** Data-layer configurations (`type_attribute`) target the underlying data shapes.
182    ///   Service-layer configurations (`client_attribute`) target the communication infrastructure generated by `tonic`.
183    /// - **Vs `client_mod_attribute`:** Attaches to the actual client code items, whereas `client_mod_attribute` wraps
184    ///   the module container boundary.
185    ///
186    /// # Examples
187    ///
188    /// ```rust,no_run
189    /// use rust_grpc_lib::build_support::Config;
190    ///
191    /// let mut config = Config::new();
192    /// // Attaches a mock trait derive directly onto the generated gRPC client struct
193    /// config = config.client_attribute("my_package.MyService", "#[derive(mockall::automock)]");
194    /// ```
195    pub fn client_attribute(self, path: &str, attribute: &str) -> Self {
196        Config {
197            builder: self.builder.client_attribute(path, attribute),
198        }
199    }
200
201    /// Add an additional attribute to the module namespace block containing the client stubs.
202    ///
203    /// # Differentiation from other attribute functions
204    /// - **Scope:** Architectural boundary/Module encapsulation. Targets the generated `pub mod my_service_client` statement.
205    /// - **Vs `client_attribute`:** `client_attribute` edits things *inside* the module (like the client struct itself).
206    ///   `client_mod_attribute` gates or tags the *entire parent module*.
207    /// - **Vs `server_mod_attribute`:** Isolates compilation properties strictly for your client implementations, ignoring server scopes.
208    ///   This is commonly used for conditional compilation (e.g., `#[cfg(feature = "client")]`) so client modules don't compile
209    ///   when the feature flag is missing.
210    ///
211    /// # Examples
212    ///
213    /// ```rust,no_run
214    /// use rust_grpc_lib::build_support::Config;
215    ///
216    /// let mut config = Config::new();
217    /// // Conditionally compiles the entire client module structure using cargo feature gates
218    /// config = config.client_mod_attribute("my_package.MyService", "#[cfg(feature = \"client\")]");
219    /// ```
220    pub fn client_mod_attribute(self, path: &str, attribute: &str) -> Self {
221        Config {
222            builder: self.builder.client_mod_attribute(path, attribute),
223        }
224    }
225
226    /// Add an additional attribute to matched standalone enum definitions.
227    ///
228    /// # Differentiation from other attribute functions
229    /// - **Scope:** Enums exclusively. It targets **only** Rust `enum` structures generated from formal,
230    ///   standalone Protobuf `enum` blocks.
231    /// - **Vs `type_attribute`:** `type_attribute` applies macros globally to both structs and enums.
232    ///   `enum_attribute` filters out message structs, ensuring your macro only runs on actual enum choices.
233    /// - **Vs `message_attribute`:** Exact opposites. `message_attribute` exclusively targets struct types,
234    ///   while `enum_attribute` strictly targets enum types.
235    /// - **The `oneof` Caveat:** Inside a generated `.rs` file, a Protobuf `oneof` block is compiled as a Rust `enum`
236    ///   to wrap exclusive variant fields. However, `enum_attribute` does **not** catch these fields. To attach macros
237    ///   to a `oneof` enum structure, you must explicitly use `type_attribute` paired with the exact field path.
238    ///
239    /// # Examples
240    ///
241    /// ```rust,no_run
242    /// use rust_grpc_lib::build_support::Config;
243    ///
244    /// let mut config = Config::new();
245    /// // Derives enum-specific utilities (like string mapping) only on standalone enums
246    /// config.enum_attribute("my_package.UserRole", "#[derive(strum::EnumString, strum::Display)]");
247    /// ```
248    pub fn enum_attribute(self, path: &str, attribute: &str) -> Self {
249        Config {
250            builder: self.builder.enum_attribute(path, attribute),
251        }
252    }
253
254    /// Add an additional attribute to individual struct fields or enum variants.
255    ///
256    /// # Differentiation from other attribute functions
257    /// - **Scope:** Sub-item block placement. It injects code **inside** the generated data types, directly above
258    ///   individual struct fields or the variants inside a `oneof` enum block.
259    /// - **Vs `type_attribute` / `message_attribute`:** Those methods append attributes to the top level of the
260    ///   type declaration. `field_attribute` is used exclusively for property-level tuning, such as field skipping,
261    ///   renaming, default values, or target serialization hooks.
262    ///
263    /// # Examples
264    ///
265    /// ```rust,no_run
266    /// use rust_grpc_lib::build_support::Config;
267    ///
268    /// let mut config = Config::new();
269    /// // Injects a serde rule directly above the `hashed_password` struct field
270    /// config = config.field_attribute("my_package.User.hashed_password", "#[serde(skip_serializing)]");
271    /// ```
272    pub fn field_attribute(self, path: &str, attribute: &str) -> Self {
273        Config {
274            builder: self.builder.field_attribute(path, attribute),
275        }
276    }
277
278    /// Add an additional attribute to matched messages specifically.
279    ///
280    /// # Differentiation from other attribute functions
281    /// - **Scope:** Strict struct-only type-level macro. It targets **only** Rust `struct` definitions generated
282    ///   from Protobuf messages.
283    /// - **Vs `type_attribute`:** It automatically ignores all `enum` items and `oneof` enums. This is useful
284    ///   if you use a derive macro that works safely on structs but panics or fails when applied to enums.
285    /// - **Vs `field_attribute`:** Modifies the top-level message definition item, not individual fields within it.
286    ///
287    /// # Examples
288    ///
289    /// ```rust,no_run
290    /// use rust_grpc_lib::build_support::Config;
291    ///
292    /// let mut config = Config::new();
293    /// // Applies a struct-specific macro only to the "User" message struct, ignoring enums
294    /// config = config.message_attribute("my_package.User", "#[derive(SomeStructOnlyMacro)]");
295    /// ```
296    pub fn message_attribute(self, path: &str, attribute: &str) -> Self {
297        Config {
298            builder: self.builder.message_attribute(path, attribute),
299        }
300    }
301
302    /// Add an additional attribute to generated gRPC server trait implementations.
303    ///
304    /// # Differentiation from other attribute functions
305    /// - **Scope:** gRPC-specific tooling. Targets the generated trait defined for the server interface (e.g., `pub trait MyService`)
306    ///   and the generated service server dispatcher (`pub struct MyServiceServer<T>`).
307    /// - **Vs `client_attribute`:** Modifies only the server side of the contract, ignoring the client dispatcher code block.
308    /// - **Vs `server_mod_attribute`:** Attaches to specific inner server structs/traits, while `server_mod_attribute` applies
309    ///   attributes to the outer enclosing module scope.
310    ///
311    /// # Examples
312    ///
313    /// ```rust,no_run
314    /// use rust_grpc_lib::build_support::Config;
315    ///
316    /// let mut config = Config::new();
317    /// // Forces a specific custom handling or restriction macro directly onto the server trait
318    /// config = config.server_attribute("my_package.MyService", "#[custom_server_gate]");
319    /// ```
320    pub fn server_attribute(self, path: &str, attribute: &str) -> Self {
321        Config {
322            builder: self.builder.server_attribute(path, attribute),
323        }
324    }
325
326    /// Add an additional attribute to the module namespace block containing the server stubs.
327    ///
328    /// # Differentiation from other attribute functions
329    /// - **Scope:** Architectural boundary/Module encapsulation. Targets the generated `pub mod my_service_server` statement.
330    /// - **Vs `server_attribute`:** Modifies the parent module containing the server structures instead of modifying the internal server trait elements.
331    /// - **Vs `client_mod_attribute`:** Isolates compilation traits specifically for the server environment.
332    ///   This is typically used to append module-wide documentation rules, clippy lint overrides, or conditional features
333    ///   (e.g., `#[cfg(feature = "server")]`) to avoid tracking server logic on clean client dependencies.
334    ///
335    /// # Examples
336    ///
337    /// ```rust,no_run
338    /// use rust_grpc_lib::build_support::Config;
339    ///
340    /// let mut config = Config::new();
341    /// // Disables specific Clippy warnings across the entire generated server codebase module
342    /// config = config.server_mod_attribute("my_package.MyService", "#[allow(clippy::too_many_arguments)]");
343    /// ```
344    pub fn server_mod_attribute(self, path: &str, attribute: &str) -> Self {
345        Config {
346            builder: self.builder.server_mod_attribute(path, attribute),
347        }
348    }
349
350    /// Add an additional attribute to matched messages, enums, and `oneof` types.
351    ///
352    /// # Differentiation from other attribute functions
353    /// - **Scope:** Broadest type-level macro. Applies to **both** `struct` definitions (generated from
354    ///   Protobuf messages) and `enum` definitions (generated from standalone enums or `oneof` groupings).
355    /// - **Vs `message_attribute`:** `type_attribute` modifies both structs and enums. Use `message_attribute`
356    ///   if you want to target structs exclusively.
357    /// - **Vs `field_attribute`:** Operates on the root container definition (`struct MyMessage`), whereas
358    ///   `field_attribute` operates on properties inside the container (`pub my_field: String`).
359    ///
360    /// # Examples
361    ///
362    /// ```rust,no_run
363    /// use rust_grpc_lib::build_support::Config;
364    ///
365    /// let mut config = Config::new();
366    /// // Derives Serialize/Deserialize for EVERY message and enum under the package
367    /// config = config.type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]");
368    /// ```
369    pub fn type_attribute(self, path: &str, attribute: &str) -> Self {
370        Config {
371            builder: self.builder.type_attribute(path, attribute),
372        }
373    }
374}
375
376impl Default for Config {
377    fn default() -> Self {
378        Self::new()
379    }
380}
381
382/// Invoke [`tonic_prost_build::Builder::compile_protos`] on the given proto files.
383///
384/// # Safety
385///
386/// This function calls [`std::env::set_var`] to point `PROTOC` at the vendored
387/// binary. That is only safe when no other threads are reading the environment
388/// concurrently. Cargo runs `build.rs` single-threaded, so this is safe in
389/// normal usage.
390fn compile(parent_dir: &str, proto_files: &[String], config: Config) -> Result<(), Box<dyn Error>> {
391    unsafe {
392        // SAFETY: build scripts run single-threaded; `set_var` is only unsafe
393        // in multi-threaded contexts.
394        env::set_var("PROTOC", protoc_bin_vendored::protoc_bin_path()?);
395    }
396
397    config
398        .builder
399        .compile_protos(proto_files, &[parent_dir.to_string()])?;
400
401    Ok(())
402}