Skip to content

Commit 1a2bee6

Browse files
authored
Add derive macro for BuilderLite, add #[non_exhaustive] to some enums and structs (#2614)
* Add a derive procmacro to implement the Builder Lite pattern for a struct * Add `#[non_exhaustive]` and derive `BuilderLite` where necessary in I2C module * Add `#[non_exhaustive]` and derive `BuilderLite` where necessary in UART module * Add `#[non_exhaustive]` and derive `BuilderLite` where necessary in SPI module * Update `CHANGELOG.md` * Fix build errors in HIL tests * Fix generated doc comments * Return a `ParseError` rather than panicking * Add a method to set the value to `None` for `Option` types
1 parent aeda6ac commit 1a2bee6

21 files changed

+235
-132
lines changed

esp-hal-procmacros/CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- Added the `BuilderLite` derive macro which implements the Builder Lite pattern for a struct (#2614)
13+
1214
### Fixed
1315

1416
### Changed

esp-hal-procmacros/src/lib.rs

+142-3
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,22 @@ use proc_macro::{Span, TokenStream};
5252
use proc_macro2::Ident;
5353
use proc_macro_crate::{crate_name, FoundCrate};
5454
use proc_macro_error2::abort;
55-
use syn::{parse, parse::Error as ParseError, spanned::Spanned, Item, ItemFn, ReturnType, Type};
55+
use quote::{format_ident, quote};
56+
use syn::{
57+
parse,
58+
parse::Error as ParseError,
59+
spanned::Spanned,
60+
Data,
61+
DataStruct,
62+
GenericArgument,
63+
Item,
64+
ItemFn,
65+
Path,
66+
PathArguments,
67+
PathSegment,
68+
ReturnType,
69+
Type,
70+
};
5671

5772
use self::interrupt::{check_attr_whitelist, WhiteListCaller};
5873

@@ -238,8 +253,8 @@ pub fn ram(args: TokenStream, input: TokenStream) -> TokenStream {
238253
/// esp_hal::interrupt::Priority::Priority2)]`.
239254
///
240255
/// If no priority is given, `Priority::min()` is assumed
241-
#[proc_macro_error2::proc_macro_error]
242256
#[proc_macro_attribute]
257+
#[proc_macro_error2::proc_macro_error]
243258
pub fn handler(args: TokenStream, input: TokenStream) -> TokenStream {
244259
#[derive(Debug, FromMeta)]
245260
struct MacroArgs {
@@ -341,8 +356,8 @@ pub fn load_lp_code(input: TokenStream) -> TokenStream {
341356

342357
/// Marks the entry function of a LP core / ULP program.
343358
#[cfg(any(feature = "is-lp-core", feature = "is-ulp-core"))]
344-
#[proc_macro_error2::proc_macro_error]
345359
#[proc_macro_attribute]
360+
#[proc_macro_error2::proc_macro_error]
346361
pub fn entry(args: TokenStream, input: TokenStream) -> TokenStream {
347362
lp_core::entry(args, input)
348363
}
@@ -381,3 +396,127 @@ pub fn main(args: TokenStream, item: TokenStream) -> TokenStream {
381396

382397
run(&args.meta, f, main()).unwrap_or_else(|x| x).into()
383398
}
399+
400+
/// Automatically implement the [Builder Lite] pattern for a struct.
401+
///
402+
/// This will create an `impl` which contains methods for each field of a
403+
/// struct, allowing users to easily set the values. The generated methods will
404+
/// be the field name prefixed with `with_`, and calls to these methods can be
405+
/// chained as needed.
406+
///
407+
/// ## Example
408+
///
409+
/// ```rust, no_run
410+
/// #[derive(Default)]
411+
/// enum MyEnum {
412+
/// #[default]
413+
/// A,
414+
/// B,
415+
/// }
416+
///
417+
/// #[derive(Default, BuilderLite)]
418+
/// #[non_exhaustive]
419+
/// struct MyStruct {
420+
/// enum_field: MyEnum,
421+
/// bool_field: bool,
422+
/// option_field: Option<i32>,
423+
/// }
424+
///
425+
/// MyStruct::default()
426+
/// .with_enum_field(MyEnum::B)
427+
/// .with_bool_field(true)
428+
/// .with_option_field(-5);
429+
/// ```
430+
///
431+
/// [Builder Lite]: https://matklad.github.io/2022/05/29/builder-lite.html
432+
#[proc_macro_derive(BuilderLite)]
433+
pub fn builder_lite_derive(item: TokenStream) -> TokenStream {
434+
let input = syn::parse_macro_input!(item as syn::DeriveInput);
435+
436+
let span = input.span();
437+
let ident = input.ident;
438+
439+
let mut fns = Vec::new();
440+
if let Data::Struct(DataStruct { fields, .. }) = &input.data {
441+
for field in fields {
442+
let field_ident = field.ident.as_ref().unwrap();
443+
let field_type = &field.ty;
444+
445+
let function_ident = format_ident!("with_{}", field_ident);
446+
447+
let maybe_path_type = extract_type_path(field_type)
448+
.and_then(|path| extract_option_segment(path))
449+
.and_then(|path_seg| match path_seg.arguments {
450+
PathArguments::AngleBracketed(ref params) => params.args.first(),
451+
_ => None,
452+
})
453+
.and_then(|generic_arg| match *generic_arg {
454+
GenericArgument::Type(ref ty) => Some(ty),
455+
_ => None,
456+
});
457+
458+
let (field_type, field_assigns) = if let Some(inner_type) = maybe_path_type {
459+
(inner_type, quote! { Some(#field_ident) })
460+
} else {
461+
(field_type, quote! { #field_ident })
462+
};
463+
464+
fns.push(quote! {
465+
#[doc = concat!(" Assign the given value to the `", stringify!(#field_ident) ,"` field.")]
466+
pub fn #function_ident(mut self, #field_ident: #field_type) -> Self {
467+
self.#field_ident = #field_assigns;
468+
self
469+
}
470+
});
471+
472+
if maybe_path_type.is_some() {
473+
let function_ident = format_ident!("with_{}_none", field_ident);
474+
fns.push(quote! {
475+
#[doc = concat!(" Set the value of `", stringify!(#field_ident), "` to `None`.")]
476+
pub fn #function_ident(mut self) -> Self {
477+
self.#field_ident = None;
478+
self
479+
}
480+
});
481+
}
482+
}
483+
} else {
484+
return ParseError::new(
485+
span,
486+
"#[derive(Builder)] is only defined for structs, not for enums or unions!",
487+
)
488+
.to_compile_error()
489+
.into();
490+
}
491+
492+
let implementation = quote! {
493+
#[automatically_derived]
494+
impl #ident {
495+
#(#fns)*
496+
}
497+
};
498+
499+
implementation.into()
500+
}
501+
502+
// https://stackoverflow.com/a/56264023
503+
fn extract_type_path(ty: &Type) -> Option<&Path> {
504+
match *ty {
505+
Type::Path(ref typepath) if typepath.qself.is_none() => Some(&typepath.path),
506+
_ => None,
507+
}
508+
}
509+
510+
// https://stackoverflow.com/a/56264023
511+
fn extract_option_segment(path: &Path) -> Option<&PathSegment> {
512+
let idents_of_path = path.segments.iter().fold(String::new(), |mut acc, v| {
513+
acc.push_str(&v.ident.to_string());
514+
acc.push('|');
515+
acc
516+
});
517+
518+
vec!["Option|", "std|option|Option|", "core|option|Option|"]
519+
.into_iter()
520+
.find(|s| idents_of_path == *s)
521+
.and_then(|_| path.segments.last())
522+
}

esp-hal/CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2222
- Added `esp_hal::psram::psram_raw_parts` (#2546)
2323
- The timer drivers `OneShotTimer` & `PeriodicTimer` have `into_async` and `new_typed` methods (#2586)
2424
- `timer::Timer` trait has three new methods, `wait`, `async_interrupt_handler` and `peripheral_interrupt` (#2586)
25+
- Configuration structs in the I2C, SPI, and UART drivers now implement the Builder Lite pattern (#2614)
2526

2627
### Changed
2728

@@ -42,6 +43,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4243
- The timer drivers `OneShotTimer` & `PeriodicTimer` now have a `Mode` parameter and type erase the underlying driver by default (#2586)
4344
- `timer::Timer` has new trait requirements of `Into<AnyTimer>`, `'static` and `InterruptConfigurable` (#2586)
4445
- `systimer::etm::Event` no longer borrows the alarm indefinitely (#2586)
46+
- A number of public enums and structs in the I2C, SPI, and UART drivers have been marked with `#[non_exhaustive]` (#2614)
4547

4648
### Fixed
4749

esp-hal/src/dma/mod.rs

+1-5
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,7 @@
2828
//!
2929
//! let mut spi = Spi::new_with_config(
3030
//! peripherals.SPI2,
31-
//! Config {
32-
//! frequency: 100.kHz(),
33-
//! mode: SpiMode::Mode0,
34-
//! ..Config::default()
35-
//! },
31+
//! Config::default().with_frequency(100.kHz()).with_mode(SpiMode::Mode0)
3632
//! )
3733
//! .with_sck(sclk)
3834
//! .with_mosi(mosi)

esp-hal/src/i2c/master/mod.rs

+7-2
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ const MAX_ITERATIONS: u32 = 1_000_000;
8686
/// I2C-specific transmission errors
8787
#[derive(Debug, Clone, Copy, PartialEq)]
8888
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
89+
#[non_exhaustive]
8990
pub enum Error {
9091
/// The transmission exceeded the FIFO size.
9192
ExceedingFifo,
@@ -106,14 +107,15 @@ pub enum Error {
106107
/// I2C-specific configuration errors
107108
#[derive(Debug, Clone, Copy, PartialEq)]
108109
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
110+
#[non_exhaustive]
109111
pub enum ConfigError {}
110112

111-
#[derive(PartialEq)]
112113
// This enum is used to keep track of the last/next operation that was/will be
113114
// performed in an embedded-hal(-async) I2c::transaction. It is used to
114115
// determine whether a START condition should be issued at the start of the
115116
// current operation and whether a read needs an ack or a nack for the final
116117
// byte.
118+
#[derive(PartialEq)]
117119
enum OpKind {
118120
Write,
119121
Read,
@@ -217,6 +219,7 @@ enum Ack {
217219
Ack = 0,
218220
Nack = 1,
219221
}
222+
220223
impl From<u32> for Ack {
221224
fn from(ack: u32) -> Self {
222225
match ack {
@@ -226,15 +229,17 @@ impl From<u32> for Ack {
226229
}
227230
}
228231
}
232+
229233
impl From<Ack> for u32 {
230234
fn from(ack: Ack) -> u32 {
231235
ack as u32
232236
}
233237
}
234238

235239
/// I2C driver configuration
236-
#[derive(Debug, Clone, Copy)]
240+
#[derive(Debug, Clone, Copy, procmacros::BuilderLite)]
237241
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
242+
#[non_exhaustive]
238243
pub struct Config {
239244
/// The I2C clock frequency.
240245
pub frequency: HertzU32,

esp-hal/src/spi/master.rs

+4-6
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,7 @@
4545
//!
4646
//! let mut spi = Spi::new_with_config(
4747
//! peripherals.SPI2,
48-
//! Config {
49-
//! frequency: 100.kHz(),
50-
//! mode: SpiMode::Mode0,
51-
//! ..Config::default()
52-
//! },
48+
//! Config::default().with_frequency(100.kHz()).with_mode(SpiMode::Mode0)
5349
//! )
5450
//! .with_sck(sclk)
5551
//! .with_mosi(mosi)
@@ -93,6 +89,7 @@ use crate::{
9389
#[cfg(gdma)]
9490
#[derive(Debug, EnumSetType)]
9591
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
92+
#[non_exhaustive]
9693
pub enum SpiInterrupt {
9794
/// Indicates that the SPI transaction has completed successfully.
9895
///
@@ -423,8 +420,9 @@ impl Address {
423420
}
424421

425422
/// SPI peripheral configuration
426-
#[derive(Clone, Copy, Debug)]
423+
#[derive(Clone, Copy, Debug, procmacros::BuilderLite)]
427424
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
425+
#[non_exhaustive]
428426
pub struct Config {
429427
/// SPI clock frequency
430428
pub frequency: HertzU32,

esp-hal/src/spi/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ pub mod slave;
1717
/// SPI errors
1818
#[derive(Debug, Clone, Copy, PartialEq)]
1919
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
20+
#[non_exhaustive]
2021
pub enum Error {
2122
/// Error occurred due to a DMA-related issue.
2223
DmaError(DmaError),

0 commit comments

Comments
 (0)