micropb_gen/lib.rs
1#![warn(missing_docs)]
2//! `micropb-gen` compiles `.proto` files into Rust code. It is intended to be used inside
3//! `build.rs` for build-time code generation.
4//!
5//! Unlike other Protobuf code generators in the Rust ecosystem, `micropb` is aimed for constrained
6//! environments without an allocator.
7//!
8//! The entry point of this crate is the [`Generator`] type.
9//!
10//! For info on the "library layer" of `micropb-gen`, see [`micropb`].
11//!
12//! # Getting Started
13//!
14//! Add `micropb` crates to your `Cargo.toml`:
15//! ```protobuf
16//! [dependencies]
17//! # Allow types from `heapless` to be used for container fields
18//! micropb = { version = "0.3.0", features = ["container-heapless"] }
19//!
20//! [build-dependencies]
21//! micropb-gen = "0.3.0"
22//! ```
23//!
24//! Then, place your `.proto` file into the project's root directory:
25//! ```proto
26//! // example.proto
27//! message Example {
28//! int32 field1 = 1;
29//! bool field2 = 2;
30//! double field3 = 3;
31//! }
32//! ```
33//!
34//! `micropb-gen` requires `protoc` to build `.proto` files, so [install
35//! `protoc`](https://grpc.io/docs/protoc-installation) and add it to your PATH, then invoke the
36//! code generator in `build.rs`:
37//!
38//! ```rust,no_run
39//! let mut generator = micropb_gen::Generator::new();
40//! // Compile example.proto into a Rust module
41//! generator.compile_protos(&["example.proto"], std::env::var("OUT_DIR").unwrap() + "/example.rs").unwrap();
42//! ```
43//!
44//! Finally, include the generated file in your code:
45//! ```rust,ignore
46//! // main.rs
47//! use micropb::{MessageDecode, MessageEncode, PbEncoder};
48//!
49//! mod example {
50//! #![allow(clippy::all)]
51//! #![allow(nonstandard_style, unused, irrefutable_let_patterns)]
52//! // Let's assume that Example is the only message define in the .proto file that has been
53//! // converted into a Rust struct
54//! include!(concat!(env!("OUT_DIR"), "/example.rs"));
55//! }
56//!
57//! let example = example::Example {
58//! field1: 12,
59//! field2: true,
60//! field3: 0.234,
61//! };
62//!
63//! // Maximum size of the message type on the wire, scaled to the next power of 2 for heapless::Vec
64//! const CAPACITY: usize = example::Example::MAX_SIZE.unwrap().next_power_of_two();
65//! // For the example message above we can use a smaller capacity
66//! // const CAPACITY: usize = 32;
67//!
68//! // Use heapless::Vec as the output stream and build an encoder around it
69//! let mut encoder = PbEncoder::new(micropb::heapless::Vec::<u8, CAPACITY>::new());
70//!
71//! // Compute the size of the `Example` on the wire
72//! let _size = example.compute_size();
73//! // Encode the `Example` to the data stream
74//! example.encode(&mut encoder).expect("Vec over capacity");
75//!
76//! // Decode a new instance of `Example` into a new struct
77//! let mut new = example::Example::default();
78//! let data = encoder.as_writer().as_slice();
79//! new.decode_from_bytes(data).expect("decoding failed");
80//! assert_eq!(example, new);
81//! ```
82//!
83//! # Messages
84//!
85//! Protobuf messages are translated directly into Rust structs, and each message field translates into a Rust field.
86//!
87//! Given the following Protobuf definition:
88//! ```proto
89//! syntax = "proto3";
90//!
91//! package example;
92//!
93//! message Example {
94//! int32 f_int32 = 1;
95//! int64 f_int64 = 2;
96//! uint32 f_uint32 = 3;
97//! uint64 f_uint64 = 4;
98//! sint32 f_sint32 = 5;
99//! sint64 f_sint64 = 6;
100//! bool f_bool = 7;
101//! fixed32 f_fixed32 = 8;
102//! fixed64 f_fixed64 = 9;
103//! sfixed32 f_sfixed32 = 10;
104//! sfixed64 f_sfixed64 = 11;
105//! float f_float = 12;
106//! double f_double = 13;
107//! }
108//! ```
109//!
110//! `micropb-gen` will generate the following Rust structs and APIs:
111//! ```rust,ignore
112//! pub mod example_ {
113//! #[derive(Debug, Clone, Copy)]
114//! pub struct Example {
115//! pub f_int32: i32,
116//! pub f_int64: i64,
117//! pub f_uint32: u32,
118//! pub f_uint64: u64,
119//! pub f_sint32: i32,
120//! pub f_sint64: i64,
121//! pub f_bool: bool,
122//! pub f_fixed32: u32,
123//! pub f_fixed64: u64,
124//! pub f_sfixed32: u32,
125//! pub f_sfixed64: u64,
126//! pub f_float: f32,
127//! pub f_double: f64,
128//! }
129//!
130//! impl Example {
131//! /// Return reference to f_int32
132//! pub fn f_int32(&self) -> &i32;
133//! /// Return mutable reference to f_int32
134//! pub fn mut_f_int32(&mut self) -> &mut i32;
135//! /// Set value of f_int32
136//! pub fn set_f_int32(&mut self, val: i32) -> &mut Self;
137//! /// Builder method that sets f_int32. Useful for initializing the message.
138//! pub fn init_f_int32(mut self, val: i32) -> Self;
139//!
140//! // Same APIs for the other singular fields
141//! }
142//!
143//! impl Default for Example { /* ... */ }
144//!
145//! impl PartialEq for Example { /* ... */ }
146//!
147//! impl micropb::MessageEncode for Example { /* ... */ }
148//!
149//! impl micropb::MessageDecode for Example { /* ... */ }
150//! }
151//! ```
152//!
153//! The generated [`MessageDecode`](micropb::MessageEncode) and
154//! [`MessageEncode`](micropb::MessageDecode) implementations provide APIs for decoding, encoding,
155//! and computing the size of `Example`.
156//!
157//! Implementations or derives for `Default`, `Clone`, `PartialEq`, and `Debug` are also provided.
158//! `Copy` derives are generated for messages consisting entirely of copyable fields.
159//!
160//! ## Optional Fields
161//!
162//! While the obvious choice for representing optional fields is [`Option`], this is not actually
163//! ideal in embedded systems because `Option<T>` actually takes up twice as much space as `T` for
164//! many types, such as `u32` and `i32`. Instead, **`micropb` tracks the presence of all optional
165//! fields of a message in a separate bitfield called a _hazzer_**, which is usually small enough to
166//! fit into the padding. Field presence can either be queried directly from the hazzer or from
167//! message APIs that return `Option`.
168//!
169//! For example, given the following Protobuf message:
170//! ```proto
171//! message Example {
172//! optional int32 f_int32 = 1;
173//! optional int64 f_int64 = 2;
174//! optional bool f_bool = 3;
175//! }
176//! ```
177//!
178//! `micropb-gen` generates the following Rust struct and APIs:
179//! ```rust,ignore
180//! pub struct Example {
181//! pub f_int32: i32,
182//! pub f_int64: i64,
183//! pub f_bool: bool,
184//!
185//! pub _has: Example_::_Hazzer,
186//! }
187//!
188//! impl Example {
189//! /// Return reference to f_int32 as an Option
190//! pub fn f_int32(&self) -> Option<&i32>;
191//! /// Return mutable reference to f_int32 as an Option
192//! pub fn mut_f_int32(&mut self) -> Option<&mut i32>;
193//! /// Set value and presence of f_int32
194//! pub fn set_f_int32(&mut self, val: i32) -> &mut Self;
195//! /// Clear presence of f_int32
196//! pub fn clear_f_int32(&mut self) -> &mut Self;
197//! /// Take f_int32 and return it
198//! pub fn take_f_int32(&mut self) -> Option<i32>;
199//! /// Builder method that sets f_int32. Useful for initializing the message.
200//! pub fn init_f_int32(mut self, val: i32) -> Self;
201//!
202//! // Same APIs for other optional fields
203//! }
204//!
205//! pub mod Example_ {
206//! /// Tracks whether the optional fields are present
207//! #[derive(Debug, Default, Clone, PartialEq, Copy)]
208//! pub struct _Hazzer([u8; 1]);
209//!
210//! impl _Hazzer {
211//! /// Create an empty Hazzer with all fields cleared
212//! pub const fn _new() -> Self;
213//!
214//! /// Query presence of f_int32
215//! pub const fn f_int32(&self) -> bool;
216//! /// Set presence of f_int32
217//! pub const fn set_f_int32(&mut self) -> &mut Self;
218//! /// Clear presence of f_int32
219//! pub const fn clear_f_int32(&mut self) -> &mut Self;
220//! /// Builder method that toggles on the presence of f_int32. Useful for initializing the Hazzer.
221//! pub const fn init_f_int32(mut self) -> Self;
222//!
223//! // Same APIs for other optional fields
224//! }
225//! }
226//!
227//! // trait impls, decode/encode logic, etc
228//! ```
229//!
230//! ### Note on Initialization
231//!
232//! **A field will be considered empty (and ignored by the encoder) if its bit in the hazzer is not
233//! set, _even if the field itself has been written_.** The following is an easy way to initialize a
234//! message with all optional fields set:
235//! ```rust,ignore
236//! Example::default().init_f_int32(4).init_f_int64(-5).init_f_bool(true)
237//! ```
238//!
239//! Alternatively, we can initialize the message using the constructor:
240//! ```rust,ignore
241//! Example {
242//! f_int32: 4,
243//! f_int64: -5,
244//! f_bool: true,
245//! // initialize the hazzer with all fields set to true
246//! // without initializing the hazzer, all fields in Example will be considered unset
247//! _has: Example_::_Hazzer::default()
248//! .init_f_int32()
249//! .init_f_int64()
250//! .init_f_bool()
251//! }
252//! ```
253//!
254//! ### Fallback to [`Option`]
255//!
256//! By default, optional fields are represented by bitfields, as shown above. If an optional field
257//! is configured to be boxed via [`Config::boxed`], it will instead be represented as an `Option`,
258//! because `Option<Box<T>>` doesn't take up extra space compared to `Box<T>`. To override these default
259//! behaviours, see [`Config::optional_repr`].
260//!
261//! ### Required fields
262//!
263//! The generator treats required fields exactly the same way it treats optional fields.
264//!
265//! ## Message fields
266//!
267//! Message fields are generated as the corresponding Rust struct. If the message field has no
268//! modifier in `proto3`, it will be treated as an optional field. Cyclical references between
269//! parent message types and field types will be broken by automatically boxing the field to
270//! prevent infinite-sized structs.
271//!
272//! ## Oneof Fields
273//!
274//! Protobuf oneofs are translated into Rust enums. The enum type is defined in an internal
275//! module under the message, and its type name is the same as the name of the oneof field.
276//!
277//! For example, given this Protobuf definition:
278//! ```proto
279//! message Example {
280//! oneof number {
281//! int32 int = 1;
282//! float decimal = 2;
283//! }
284//! }
285//! ```
286//!
287//! `micropb-gen` generates the following definition:
288//! ```rust,no_run
289//! #[derive(Debug, Clone, PartialEq, Copy)]
290//! pub struct Example {
291//! pub number: Option<Example_::Number>,
292//! }
293//!
294//! pub mod Example_ {
295//! #[derive(Debug, Clone, PartialEq, Copy)]
296//! pub enum Number {
297//! Int(i32),
298//! Decimal(f32),
299//! }
300//! }
301//! ```
302//!
303//! ## Repeated, `map`, `string`, and `bytes` Fields
304//!
305//! Repeated, `map`, `string`, and `bytes` fields need to be represented as Rust "container" types,
306//! since they contain multiple elements or bytes. Normally standard types like `String` and `Vec`
307//! are used, but they aren't available in no-alloc environments. Instead, we need stack-allocated
308//! containers with fixed capacity. Since there is no defacto standard for such containers in Rust,
309//! **users are expected to configure the code generator with their own container types** (see
310//! [`Config`] for more details).
311//!
312//! For example, given the following Protobuf definition:
313//! ```proto
314//! message Containers {
315//! string f_string = 1;
316//! bytes f_bytes = 2;
317//! repeated int32 f_repeated = 3;
318//! map<int32, int64> f_map = 4;
319//! }
320//! ```
321//!
322//! and the following configuration in `build.rs`:
323//! ```rust,no_run
324//! let mut generator = micropb_gen::Generator::new();
325//! // Configure our own container types
326//! generator.configure(".",
327//! micropb_gen::Config::new()
328//! .string_type("crate::MyString<$N>")
329//! .bytes_type("crate::MyVec<u8, $N>")
330//! .vec_type("crate::MyVec<$T, $N>")
331//! .map_type("crate::MyMap<$K, $V, $N>")
332//! );
333//!
334//! // We can also use container types from `heapless`, which have fixed capacity
335//! generator.use_container_heapless();
336//!
337//! // Same shorthand exists for containers from `arrayvec` or `alloc`
338//! // generator.use_container_arrayvec();
339//! // generator.use_container_alloc();
340//!
341//!
342//! // Since we're using fixed containers, we need to specify the max capacity of each field.
343//! // For simplicity, configure capacity of all repeated/map fields to 4 and string/bytes to 8.
344//! generator.configure(".", micropb_gen::Config::new().max_len(4).max_bytes(8));
345//! ```
346//!
347//! The following Rust struct will be generated:
348//! ```rust,no_run
349//! # use micropb::heapless as heapless;
350//! pub struct Containers {
351//! f_string: heapless::String<8>,
352//! f_bytes: heapless::Vec<u8, 8>,
353//! f_repeated: heapless::Vec<i32, 4>,
354//! f_map: heapless::FnvIndexMap<i32, i64, 4>,
355//! }
356//! ```
357//!
358//! For **decoding**, container types should implement [`PbVec`](micropb::PbVec) (repeated fields),
359//! [`PbString`](micropb::PbString), [`PbBytes`](micropb::PbBytes), or [`PbMap`](micropb::PbMap)
360//! For convenience, [`micropb`] comes with built-in implementations of the container traits for
361//! types from [`heapless`](https://docs.rs/heapless/latest/heapless),
362//! [`arrayvec`](https://docs.rs/arrayvec/latest/arrayvec), and
363//! [`alloc`](https://doc.rust-lang.org/alloc), as well as implementations on `[u8; N]` arrays and
364//! [`FixedLenString`](micropb::FixedLenString).
365//!
366//! For **encoding**, container types need to dereference into `&[T]` (repeated fields), `&str`, or
367//! `&[u8]`. Maps just need to iterate through key-value pairs.
368//!
369//! ## Message Lifetime
370//!
371//! A message struct may have up to one lifetime parameter. `micropb-gen` automatically generates
372//! the lifetime parameter for each message by checking if there's a lifetime in any of the fields.
373//!
374//! For example, given the Protobuf file from the previous section and the following `build.rs`
375//! config:
376//! ```rust,no_run
377//! # use micropb_gen::{Generator, Config, config::CustomField};
378//! # let mut generator = Generator::new();
379//! // Use `Cow` as container type with lifetime of 'a
380//! generator.configure(".",
381//! Config::new()
382//! .string_type("alloc::borrow::Cow<'a, str>")
383//! .bytes_type("alloc::borrow::Cow<'a, [u8]>")
384//! .vec_type("alloc::borrow::Cow<'a, [$T]>")
385//! );
386//! // Use a custom type for the `f_map` field, also with lifetime of 'a
387//! generator.configure(".Containers.f_map",
388//! Config::new().custom_field(CustomField::from_type("MyField<'a>"))
389//! );
390//! ```
391//!
392//! `micropb-gen` generates the following struct:
393//! ```rust,no_run
394//! # extern crate alloc;
395//! # struct MyField<'a>(&'a u8);
396//! pub struct Containers<'a> {
397//! f_string: alloc::borrow::Cow<'a, str>,
398//! f_bytes: alloc::borrow::Cow<'a, [u8]>,
399//! f_repeated: alloc::borrow::Cow<'a, [i32]>,
400//! f_map: MyField<'a>,
401//! }
402//! ```
403//!
404//! Note that message types can only have a single lifetime, so don't mix multiple lifetime
405//! identifiers in your configuration.
406//!
407//! # Enums
408//!
409//! Protobuf enums are translated into "open" enums in Rust, rather than normal Rust enums. This is
410//! because proto3 requires enums to store unrecognized values, which is only possible with open
411//! enums.
412//!
413//! For example, given this Protobuf enum:
414//! ```proto
415//! enum Language {
416//! RUST = 0,
417//! C = 1,
418//! CPP = 2,
419//! }
420//! ```
421//!
422//! `micropb-gen` generates the following Rust definition:
423//! ```rust,ignore
424//! #[derive(Debug, Clone, Default, Copy, PartialEq, Eq, Hash)]
425//! #[repr(transparent)]
426//! pub struct Language(pub i32);
427//!
428//! impl Language {
429//! // Default value
430//! pub const Rust: Self = Self(0);
431//! pub const C: Self = Self(1);
432//! pub const Cpp: Self = Self(2);
433//! }
434//!
435//! impl From<i32> for Language { /* .. */ }
436//! ```
437//!
438//! # Packages and Modules
439//!
440//! `micropb-gen` translates Protobuf package names into Rust modules by appending an underscore.
441//!
442//! For example, given the following Protobuf file:
443//! ```proto
444//! package foo.bar;
445//!
446//! // Protobuf contents
447//! ```
448//!
449//! The generated Rust file will look like:
450//! ```rust,ignore
451//! pub mod foo_ {
452//! pub mod bar_ {
453//! // Generated code lives here
454//! }
455//! }
456//! ```
457//!
458//! If a Protobuf file does not have a package specifier, the generated code will instead live in
459//! the root module
460//!
461//! Message names are also translated into Rust modules by appending an underscore. For example,
462//! code generated from oneofs and nested messages within the `Name` message will live in the
463//! `Name_` module.
464//!
465//! # Configuring the Generator
466//!
467//! One of `micropb-gen`'s main features is its granular configuration system, which allows users
468//! to control how code is generated at the level of the module, message, or even individual
469//! fields. See [`Generator::configure`] and [`Config`] for more info on the configuration system.
470//!
471//! ## Notable Configurations
472//!
473//! - **Integer size**: Controls the width of the integer types used to represent [integer
474//! fields](Config::int_size). This can also be done for [enums](Config::enum_int_size).
475//!
476//! - **Attributes**: Apply custom attributes to [fields](Config::field_attributes) and
477//! [messages](Config::type_attributes).
478//!
479//! - **Custom fields**: Substitute your own type into the generated code, allowing complete
480//! control over the encode and decode behaviour. Can be applied to [normal
481//! fields](Config::custom_field) or [unknown fields](Config::unknown_handler).
482//!
483//! - **Max container size**: Specify the max capacity of [`string`/`bytes`
484//! fields](Config::max_bytes) as well as [repeated fields](Config::max_len), which is necessary
485//! when using fixed-capacity containers like `ArrayVec`.
486//!
487//! ## Configuration Files
488//!
489//! Configurations can be stored in TOML files rather than in `build.rs`. See
490//! [`Generator::parse_config_file`] for more info.
491
492pub mod config;
493mod generator;
494mod pathtree;
495mod utils;
496
497// This module was generated from example/file-descriptor-proto
498mod descriptor {
499 #![allow(clippy::all)]
500 #![allow(nonstandard_style, dead_code, unused_imports)]
501 include!("descriptor.rs");
502
503 pub use google_::protobuf_::*;
504}
505
506use std::{
507 collections::BTreeMap,
508 env,
509 ffi::{OsStr, OsString},
510 fmt, fs,
511 io::{self, Write},
512 path::{Path, PathBuf},
513 process::Command,
514};
515
516pub use config::Config;
517use micropb::{MessageDecode, PbDecoder};
518use pathtree::PathTree;
519use proc_macro2::TokenStream;
520
521use crate::generator::Context;
522
523#[derive(Debug, Clone, Copy, Default)]
524/// Whether to include encode and decode logic
525pub enum EncodeDecode {
526 /// Only include encode logic
527 EncodeOnly,
528 /// Only include decode logic
529 DecodeOnly,
530 #[default]
531 /// Include both encode and decode logic
532 Both,
533}
534
535impl EncodeDecode {
536 fn is_encode(self) -> bool {
537 matches!(self, Self::EncodeOnly | Self::Both)
538 }
539
540 fn is_decode(self) -> bool {
541 matches!(self, Self::DecodeOnly | Self::Both)
542 }
543}
544
545type WarningCb = fn(fmt::Arguments);
546
547fn warn_cargo_build(args: fmt::Arguments) {
548 println!("cargo::warning={args}");
549}
550
551/// Protobuf code generator
552///
553/// Use this in `build.rs` to compile `.proto` files into a Rust module.
554///
555/// The main way to control the compilation process is to call [`configure`](Generator::configure),
556/// which allows the user to customize how code is generated from Protobuf types and fields of
557/// their choosing.
558///
559/// # Note
560/// It's recommended to call one of [`use_container_alloc`](Self::use_container_alloc),
561/// [`use_container_heapless`](Self::use_container_heapless), or
562/// [`use_container_alloc`](Self::use_container_alloc) to ensure that container types are
563/// configured for `string`, `bytes`, repeated, and `map` fields. The generator will throw an
564/// error if it reaches any such field that doesn't have a container configured.
565///
566/// # Example
567/// ```no_run
568/// use micropb_gen::{Generator, Config};
569///
570/// let mut generator = Generator::new();
571/// // Use container types from `heapless`
572/// generator.use_container_heapless()
573/// // Set max length of repeated fields in .test.Data to 4
574/// .configure(".test.Data", Config::new().max_len(4))
575/// // Wrap .test.Data.value inside a Box
576/// .configure(".test.Data.value", Config::new().boxed(true));
577/// // Compile test.proto into a Rust module
578/// generator.compile_protos(
579/// &["test.proto"],
580/// std::env::var("OUT_DIR").unwrap() + "/test_proto.rs",
581/// )
582/// .unwrap();
583/// ```
584pub struct Generator {
585 pub(crate) config_tree: PathTree<Box<Config>>,
586
587 pub(crate) warning_cb: WarningCb,
588 pub(crate) extern_paths: BTreeMap<String, TokenStream>,
589 pub(crate) encode_decode: EncodeDecode,
590 pub(crate) calculate_max_size: bool,
591 pub(crate) retain_enum_prefix: bool,
592 pub(crate) format: bool,
593 pub(crate) fdset_path: Option<PathBuf>,
594 pub(crate) protoc_args: Vec<OsString>,
595 pub(crate) suffixed_package_names: bool,
596 pub(crate) single_oneof_msg_as_enum: bool,
597 pub(crate) comments_to_docs: bool,
598}
599
600#[allow(clippy::new_without_default)]
601impl Generator {
602 /// Create new generator with default settings
603 ///
604 /// By default, the generator assumes it's running inside a Cargo build script, so all warnings
605 /// will be emitted as compiler warnings. If the generator is not running inside a build
606 /// script, use [`with_warning_callback`](Self::with_warning_callback).
607 pub fn new() -> Self {
608 Self::with_warning_callback(warn_cargo_build)
609 }
610
611 /// Create a generator with a custom callback for emitting warnings
612 pub fn with_warning_callback(warning_cb: WarningCb) -> Self {
613 let config_tree = PathTree::new(Box::new(Config::default()));
614
615 Self {
616 config_tree,
617
618 warning_cb,
619 extern_paths: Default::default(),
620 encode_decode: Default::default(),
621 retain_enum_prefix: Default::default(),
622 format: true,
623 calculate_max_size: true,
624 fdset_path: Default::default(),
625 protoc_args: Default::default(),
626 suffixed_package_names: true,
627 single_oneof_msg_as_enum: false,
628 comments_to_docs: true,
629 }
630 }
631
632 fn configure_with_path<'a>(&mut self, path: impl Iterator<Item = &'a str>, config: Config) {
633 let config_slot = self.config_tree.root.add_path(path).value_mut();
634 match config_slot {
635 Some(existing) => existing.merge(&config),
636 None => *config_slot = Some(Box::new(config)),
637 }
638 }
639
640 /// Apply code generator configurations to Protobuf types and fields. See
641 /// [`Config`] for possible configuration options.
642 ///
643 /// The `proto_path` argument is a fully-qualified Protobuf path that points to a package,
644 /// type, or field in the compiled `.proto` files. The configurations are applied to the
645 /// element specified by `proto_path`, as well as its children.
646 ///
647 /// # Example
648 /// ```
649 /// # use micropb_gen::{Generator, Config, config::IntSize};
650 /// # let mut generator = micropb_gen::Generator::new();
651 /// // Configure field attributes on a specific field of a message type
652 /// generator.configure(".pkg.Message.int_field", Config::new().field_attributes("#[serde(skip)]"));
653 ///
654 /// // Configure field attributes on all fields of a message type
655 /// generator.configure(".pkg.Message", Config::new().field_attributes("#[serde(skip)]"));
656 ///
657 /// // Configure field attributes on all fields in a package
658 /// generator.configure(".pkg", Config::new().field_attributes("#[serde(skip)]"));
659 ///
660 /// // Configure field attributes on all fields
661 /// generator.configure(".", Config::new().field_attributes("#[serde(skip)]"));
662 ///
663 /// // Configure types attributes on a specific message type
664 /// generator.configure(".pkg.Message", Config::new().type_attributes("#[derive(Serialize)]"));
665 ///
666 /// // Configure boxing behaviour on an oneof in a message type
667 /// generator.configure(".pkg.Message.my_oneof", Config::new().boxed(true));
668 ///
669 /// // Configure the int size on a variant of an oneof
670 /// generator.configure(".pkg.Message.my_oneof_variant", Config::new().int_size(IntSize::S8));
671 ///
672 /// // Configure the int size of an enum
673 /// // Note that enum variants cannot be configured
674 /// generator.configure(".pkg.Enum", Config::new().enum_int_size(IntSize::S8));
675 /// ```
676 ///
677 /// # Special paths
678 /// `configure` also supports special path suffixes for configuring fields in the generated
679 /// code that don't have a corresponding Protobuf path.
680 /// ```no_run
681 /// # use micropb_gen::{Generator, Config, config::IntSize};
682 /// # let mut generator = micropb_gen::Generator::new();
683 /// // Configure the int size of the elements in a repeated field via ".elem"
684 /// generator.configure(".pkg.Message.repeated_field.elem", Config::new().int_size(IntSize::S8));
685 ///
686 /// // Configure the int size of the keys in a map field via ".key"
687 /// generator.configure(".pkg.Message.map_field.key", Config::new().int_size(IntSize::S8));
688 /// // Configure the int size of the values in a map field via ".value"
689 /// generator.configure(".pkg.Message.map_field.value", Config::new().int_size(IntSize::S16));
690 ///
691 /// // Configure the field attributes of hazzer field and the type attributes of
692 /// // the hazzer struct in the message via "._has"
693 /// generator.configure(".pkg.Message._has",
694 /// Config::new().field_attributes("#[serde(skip)]").type_attributes("#[derive(Serialize)]"));
695 ///
696 /// // Configure the field attributes for the unknown handler field of the message via "._unknown"
697 /// generator.configure(".pkg.Message._unknown", Config::new().field_attributes("#[serde(skip)]"));
698 ///
699 /// ```
700 pub fn configure(&mut self, proto_path: &str, config: Config) -> &mut Self {
701 self.configure_with_path(split_dot_prefixed_pkg_name(proto_path), config);
702 self
703 }
704
705 /// Apply one set of configurations to all provided Protobuf paths.
706 ///
707 /// See [`configure`](Self::configure) for how configurations are applied.
708 pub fn configure_many(&mut self, proto_paths: &[&str], config: Config) -> &mut Self {
709 for path in proto_paths {
710 self.configure(path, config.clone());
711 }
712 self
713 }
714
715 #[cfg(feature = "config-file")]
716 fn parse_config_bytes(&mut self, bytes: &[u8], prefix: &str) -> Result<(), toml::de::Error> {
717 let configs: std::collections::HashMap<String, Config> = toml::from_slice(bytes)?;
718 for (path, config) in configs.into_iter() {
719 let prefix_path = split_dot_prefixed_pkg_name(prefix);
720 let path = split_dot_prefixed_pkg_name(&path);
721 let full_path = prefix_path.chain(path);
722
723 self.configure_with_path(full_path, config);
724 }
725 Ok(())
726 }
727
728 /// Parse configurations from a TOML file and apply them to the specified Protobuf pacakge.
729 ///
730 /// # Example
731 ///
732 /// For example, if we have the following configuration in `build.rs`:
733 ///
734 /// ```
735 /// # use micropb_gen::{Config, config::{IntSize, OptionalRepr}};
736 /// let mut generator = micropb_gen::Generator::new();
737 /// generator.configure(
738 /// ".my.pkg.Message.int_field",
739 /// Config::new().int_size(IntSize::S16).optional_repr(OptionalRepr::Option)
740 /// );
741 /// generator.configure("my.pkg.Message.bad_field", Config::new().skip(true));
742 /// ```
743 ///
744 /// We can instead load the configuration for `.my.pkg` from a TOML file:
745 /// ```no_run
746 /// # use std::path::Path;
747 /// # let mut generator = micropb_gen::Generator::new();
748 /// generator.parse_config_file(Path::new("my.pkg.toml"), ".my.pkg")?;
749 /// # Ok::<_, std::io::Error>(())
750 /// ```
751 ///
752 /// `my.pkg.toml`
753 /// ```toml
754 /// # Each Config is represented as a table in the TOML document, keyed by the Protobuf path
755 /// ["Message.int_field"]
756 /// int_size = "S16"
757 /// optional_repr = "Option"
758 ///
759 /// ["Message.bad_field"]
760 /// skip = true
761 /// ```
762 ///
763 /// <div class="warning">Dot-separated Protobuf paths in config files MUST be wrapped in quotes
764 /// for TOML parsing to work correctly.</div>
765 #[cfg(feature = "config-file")]
766 pub fn parse_config_file(&mut self, file_path: &Path, package: &str) -> Result<(), io::Error> {
767 let file_bytes = fs::read(file_path)?;
768 self.parse_config_bytes(&file_bytes, package)
769 .map_err(io::Error::other)?;
770 Ok(())
771 }
772
773 /// Configure the generator to generate `heapless` containers for Protobuf `string`, `bytes`,
774 /// repeated, and `map` fields.
775 ///
776 /// If using this option, `micropb` should have the `container-heapless` feature enabled.
777 ///
778 /// Specifically, `heapless::String<N>` is generated for `string` fields, `heapless::Vec<u8, N>`
779 /// for `bytes` fields, `heapless::Vec<T, N>` for repeated fields, and
780 /// `heapless::FnvIndexMap<K, V, N>` for `map` fields. This uses [`configure`](Self::configure)
781 /// under the hood, so configurations set by this call can all be overriden.
782 ///
783 /// # Note
784 /// Since `heapless` containers are fixed size, [`max_len`](Config::max_len) or
785 /// [`max_bytes`](Config::max_bytes) must be set for all fields that generate these containers.
786 pub fn use_container_heapless(&mut self) -> &mut Self {
787 self.configure(
788 ".",
789 Config::new()
790 .vec_type("::micropb::heapless::Vec<$T, $N>")
791 .string_type("::micropb::heapless::String<$N>")
792 .bytes_type("::micropb::heapless::Vec<u8, $N>")
793 .map_type("::micropb::heapless::FnvIndexMap<$K, $V, $N>"),
794 );
795 self
796 }
797
798 /// Configure the generator to generate `arrayvec` containers for Protobuf `string`, `bytes`,
799 /// and repeated fields.
800 ///
801 /// If using this option, `micropb` should have the `container-arrayvec` feature enabled.
802 ///
803 /// Specifically, `arrayvec::ArrayString<N>` is generated for `string` fields,
804 /// `arrayvec::ArrayVec<u8, N>` for `bytes` fields, and `arrayvec::ArrayVec<T, N>` for repeated
805 /// fields. This uses [`configure`](Self::configure) under the hood, so configurations set by
806 /// this call can all be overriden.
807 ///
808 /// # Note
809 /// No container is configured for `map` fields, since `arrayvec` doesn't have a suitable map
810 /// type. If the .proto files contain `map` fields, [`map_type`](Config::map_type) will need to
811 /// be configured separately.
812 ///
813 /// Since `arrayvec` containers are fixed size, [`max_len`](Config::max_len) or
814 /// [`max_bytes`](Config::max_bytes) must be set for all fields that generate these containers.
815 pub fn use_container_arrayvec(&mut self) -> &mut Self {
816 self.configure(
817 ".",
818 Config::new()
819 .vec_type("::micropb::arrayvec::ArrayVec<$T, $N>")
820 .bytes_type("::micropb::arrayvec::ArrayVec<u8, $N>")
821 .string_type("::micropb::arrayvec::ArrayString<$N>"),
822 );
823 self
824 }
825
826 /// Configure the generator to generate `alloc` containers for Protobuf `string`, `bytes`,
827 /// repeated, and `map` fields.
828 ///
829 /// If using this option, `micropb` should have the `alloc` feature enabled.
830 ///
831 /// Specifically, `alloc::string::String` is generated for `string` fields,
832 /// `alloc::vec::Vec<u8>` is for `bytes` fields, `alloc::vec::Vec<T>` for repeated fields, and
833 /// `alloc::collections::BTreeMap<K, V>` for `map` fields. This uses
834 /// [`configure`](Self::configure) under the hood, so configurations set by this call can all
835 /// be overriden by future configurations.
836 pub fn use_container_alloc(&mut self) -> &mut Self {
837 self.configure(
838 ".",
839 Config::new()
840 .vec_type("::alloc::vec::Vec<$T>")
841 .bytes_type("::alloc::vec::Vec::<u8>")
842 .string_type("::alloc::string::String")
843 .map_type("::alloc::collections::BTreeMap<$K, $V>"),
844 );
845 self
846 }
847
848 /// Configure the generator to generate `std` containers for Protobuf `string`, `bytes`,
849 /// repeated, and `map` fields.
850 ///
851 /// If using this option, `micropb` should have the `std` feature enabled.
852 ///
853 /// Specifically, `std::string::String` is generated for `string` fields, `std::vec::Vec<u8>`
854 /// for `bytes` fields, `std::vec::Vec<T>` for repeated fields, and
855 /// `std::collections::HashMap<K, V>` for `map` fields. This uses
856 /// [`configure`](Self::configure) under the hood, so configurations set by this call can all
857 /// be overriden by future configurations.
858 pub fn use_container_std(&mut self) -> &mut Self {
859 self.configure(
860 ".",
861 Config::new()
862 .vec_type("::std::vec::Vec<$T>")
863 .bytes_type("::std::vec::Vec::<u8>")
864 .string_type("::std::string::String")
865 .map_type("::std::collections::HashMap<$K, $V>"),
866 );
867 self
868 }
869
870 /// Compile `.proto` files into a single Rust file.
871 ///
872 /// # Example
873 /// ```no_run
874 /// // build.rs
875 /// let mut generator = micropb_gen::Generator::new();
876 /// generator.compile_protos(&["server.proto", "client.proto"],
877 /// std::env::var("OUT_DIR").unwrap() + "/output.rs").unwrap();
878 /// ```
879 pub fn compile_protos(
880 self,
881 protos: &[impl AsRef<Path>],
882 out_filename: impl AsRef<Path>,
883 ) -> io::Result<()> {
884 let tmp;
885 let fdset_file = if let Some(fdset_path) = &self.fdset_path {
886 fdset_path.to_owned()
887 } else {
888 tmp = tempfile::tempdir()?;
889 tmp.path().join("micropb-fdset")
890 };
891
892 // Get protoc command from PROTOC env-var, otherwise just use "protoc"
893 let mut cmd = Command::new(env::var("PROTOC").as_deref().unwrap_or("protoc"));
894 cmd.arg("-o").arg(fdset_file.as_os_str());
895 if self.comments_to_docs {
896 cmd.arg("--include_source_info");
897 }
898 cmd.args(&self.protoc_args);
899
900 for proto in protos {
901 cmd.arg(proto.as_ref());
902 }
903
904 let output = cmd.output().map_err(|e| match e.kind() {
905 io::ErrorKind::NotFound => {
906 io::Error::new(e.kind(), "`protoc` was not found. Check your PATH.")
907 }
908 _ => e,
909 })?;
910 if !output.status.success() {
911 return Err(io::Error::other(format!(
912 "protoc failed: {}",
913 String::from_utf8_lossy(&output.stderr)
914 )));
915 }
916
917 self.compile_fdset_file(fdset_file, out_filename)
918 }
919
920 /// Compile a Protobuf file descriptor set into a Rust file.
921 ///
922 /// Similar to [`compile_protos`](Self::compile_protos), but it does not invoke `protoc` and
923 /// instead takes a file descriptor set.
924 pub fn compile_fdset_file(
925 self,
926 fdset_file: impl AsRef<Path>,
927 out_filename: impl AsRef<Path>,
928 ) -> io::Result<()> {
929 #[allow(unused)]
930 let format = self.format;
931
932 let bytes = fs::read(fdset_file)?;
933 let mut decoder = PbDecoder::new(bytes.as_slice());
934 let mut fdset = descriptor::FileDescriptorSet::default();
935 fdset
936 .decode(&mut decoder, bytes.len())
937 .expect("file descriptor set decode failed");
938 let code = Context::generate_fdset(self, &fdset)?;
939
940 #[cfg(feature = "format")]
941 let output = if format {
942 prettyplease::unparse(
943 &syn::parse2(code).expect("output code should be parseable as a file"),
944 )
945 } else {
946 code.to_string()
947 };
948 #[cfg(not(feature = "format"))]
949 let output = code.to_string();
950
951 let mut file = fs::File::create(out_filename)?;
952 file.write_all(output.as_bytes())?;
953
954 Ok(())
955 }
956
957 /// Determine whether the generator strips enum names from variant names.
958 ///
959 /// Protobuf enums commonly include the enum name as a prefix of variant names. `micropb`
960 /// strips this enum name prefix by default. Setting this to `true` prevents the prefix from
961 /// being stripped.
962 pub fn retain_enum_prefix(&mut self, retain_enum_prefix: bool) -> &mut Self {
963 self.retain_enum_prefix = retain_enum_prefix;
964 self
965 }
966
967 /// Determine whether the generator formats the output code.
968 ///
969 /// If the `format` feature isn't enabled, this does nothing.
970 pub fn format(&mut self, format: bool) -> &mut Self {
971 self.format = format;
972 self
973 }
974
975 /// Determine whether to generate logic for encoding and decoding Protobuf messages.
976 ///
977 /// Some applications don't need to support both encoding and decoding. This setting allows
978 /// either the encoding or decoding logic to be omitted from the output. By default, both
979 /// encoding and decoding are included.
980 ///
981 /// This setting allows omitting the `encode` or `decode` feature flag from `micropb`.
982 pub fn encode_decode(&mut self, encode_decode: EncodeDecode) -> &mut Self {
983 self.encode_decode = encode_decode;
984 self
985 }
986
987 /// When set, the file descriptor set generated by `protoc` is written to the provided path,
988 /// instead of a temporary directory.
989 pub fn file_descriptor_set_path<P: Into<PathBuf>>(&mut self, path: P) -> &mut Self {
990 self.fdset_path = Some(path.into());
991 self
992 }
993
994 /// Add an argument to the `protoc` invocation when compiling Protobuf files.
995 pub fn add_protoc_arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self {
996 self.protoc_args.push(arg.as_ref().to_owned());
997 self
998 }
999
1000 /// Declare an externally-provided Protobuf type.
1001 ///
1002 /// When compiling a `.proto` file that imports types from another `.proto` file, `micropb`
1003 /// won't compile the imported file if it's not included in the
1004 /// [`compile_protos`](Self::compile_protos) invocation. This is because the imported file may
1005 /// have already been compiled in another crate. In order to recognize externally-imported
1006 /// types, use `extern_type_path` to map the full Protobuf path of the imported type to the
1007 /// full path of the corresponding Rust type.
1008 ///
1009 /// # Example
1010 ///
1011 /// For example, let's say we have `app.proto`:
1012 /// ```proto
1013 /// // app.proto
1014 ///
1015 /// syntax = "proto3";
1016 /// package app;
1017 ///
1018 /// message App {
1019 /// time.Timestamp timestamp = 1;
1020 /// time.TZ timezone = 2;
1021 /// }
1022 /// ```
1023 ///
1024 /// `app.proto` imports from `time.proto`, which has already been compiled into the
1025 /// `time` crate:
1026 /// ```proto
1027 /// // time.proto
1028 ///
1029 /// syntax = "proto3";
1030 /// package time;
1031 ///
1032 /// message Timestamp {
1033 /// uint32 ts = 1;
1034 /// }
1035 ///
1036 /// enum TZ {
1037 /// TZ_UTC = 0;
1038 /// TZ_PST = 1;
1039 /// }
1040 /// ```
1041 ///
1042 /// For our application, we're only interested in compiling `app.proto`, since `time.proto` has
1043 /// already been compiled by another crate. As such, we need to substitute Protobuf types
1044 /// imported from `time.proto` with Rust definitions from the `time` crate.
1045 /// ```no_run
1046 /// // build.rs of app
1047 ///
1048 /// let mut generator = micropb_gen::Generator::new();
1049 /// // Substitute Timestamp message
1050 /// generator.extern_type_path(".time.Timestamp", "time::Timestamp");
1051 /// // Substitute TZ enum
1052 /// generator.extern_type_path(".time.TZ", "time::Tz");
1053 /// // Compile only app.proto, not time.proto
1054 /// generator.compile_protos(&["app.proto"], std::env::var("OUT_DIR").unwrap() + "/output.rs").unwrap();
1055 /// ```
1056 ///
1057 /// # Note
1058 /// It's technically possible to substitute in Rust types that aren't generated by `micropb-gen`.
1059 /// However, the generated code expects substituted messages to implement `MessageDecode` and
1060 /// `MessageEncode`, and substituted enums to have the "open-enum" structure.
1061 pub fn extern_type_path<P1: AsRef<str>, P2: AsRef<str>>(
1062 &mut self,
1063 proto_path: P1,
1064 rust_path: P2,
1065 ) -> &mut Self {
1066 assert!(
1067 proto_path.as_ref().starts_with('.'),
1068 "Fully-qualified Proto path must start with '.'"
1069 );
1070 self.extern_paths.insert(
1071 proto_path.as_ref().to_owned(),
1072 syn::parse_str(rust_path.as_ref()).expect("failed to tokenize extern path"),
1073 );
1074 self
1075 }
1076
1077 /// Determines whether to generate code to calculate the `MAX_SIZE` constant on each message.
1078 ///
1079 /// By default, `micropb-gen` generates code to calculate the `MAX_SIZE` associated constant
1080 /// for each message struct, which determines the max buffer size needed to encode it. If this
1081 /// is set to false, then it replaces the calculations with `Err`, effectively disabling the
1082 /// use of `MAX_SIZE`. This has no runtime impact, but it can reduce the size of the output
1083 /// file.
1084 pub fn calculate_max_size(&mut self, flag: bool) -> &mut Self {
1085 self.calculate_max_size = flag;
1086 self
1087 }
1088
1089 /// Determines whether the modules names generated from package specifiers are suffixed with an
1090 /// underscore.
1091 ///
1092 /// This is on by default. Even when off, module names like "super" and modules created from
1093 /// from message names will still be suffixed.
1094 pub fn suffixed_package_names(&mut self, suffixed: bool) -> &mut Self {
1095 self.suffixed_package_names = suffixed;
1096 self
1097 }
1098
1099 /// For messages with only a single oneof and no other fields, generate an enum representing
1100 /// the oneof rather than a struct.
1101 ///
1102 /// # Example
1103 ///
1104 /// Given the following message:
1105 /// ```proto
1106 /// message Number {
1107 /// oneof inner {
1108 /// sint32 signed = 1;
1109 /// uint32 unsigned = 2;
1110 /// float fraction = 3;
1111 /// }
1112 /// }
1113 /// ```
1114 ///
1115 /// The following enum type will be generated:
1116 /// ```no_run
1117 /// pub enum Number {
1118 /// Signed(i32),
1119 /// Unsigned(u32),
1120 /// Fraction(f32),
1121 /// None,
1122 /// }
1123 /// ```
1124 ///
1125 /// All other message structures, including those with multiple oneofs or a single oneof plus
1126 /// normal fields, will be generated as normal message structs.
1127 ///
1128 /// # Ignored configs
1129 ///
1130 /// With this option, configurations that apply to the oneof itself (`.Number.inner`) will be
1131 /// ignored. Also, [`unknown_handler`](Config::unknown_handler) will be ignored.
1132 pub fn single_oneof_msg_as_enum(&mut self, as_enum: bool) -> &mut Self {
1133 self.single_oneof_msg_as_enum = as_enum;
1134 self
1135 }
1136
1137 /// If enabled, comments in the Proto file will be used to generate doc comments on the
1138 /// messages, enums, oneofs, and fields in the generated code.
1139 ///
1140 /// Enabled by default.
1141 pub fn comments_to_docs(&mut self, flag: bool) -> &mut Self {
1142 self.comments_to_docs = flag;
1143 self
1144 }
1145}
1146
1147fn split_pkg_name(name: &str) -> impl Iterator<Item = &str> {
1148 // ignore empty segments, so empty pkg name points to root node
1149 name.split('.').filter(|seg| !seg.is_empty())
1150}
1151
1152fn split_dot_prefixed_pkg_name(mut name: &str) -> impl Iterator<Item = &str> {
1153 if name.starts_with('.') {
1154 name = &name[1..];
1155 }
1156 split_pkg_name(name)
1157}