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}