-
-
Notifications
You must be signed in to change notification settings - Fork 567
/
Copy pathwidget_builder.rs
162 lines (138 loc) · 6.35 KB
/
widget_builder.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
use proc_macro2::{Ident, Literal, TokenStream as TokenStream2};
use quote::ToTokens;
use syn::spanned::Spanned;
use syn::{Attribute, Data, DeriveInput, Field, PathArguments, Type};
/// Check if a specified `#[widget_builder target]` attribute can be found in the list
fn has_attribute(attrs: &[Attribute], target: &str) -> bool {
attrs
.iter()
.filter(|attr| attr.path().to_token_stream().to_string() == "widget_builder")
.any(|attr| attr.meta.require_list().is_ok_and(|list| list.tokens.to_string() == target))
}
/// Make setting strings easier by allowing all types that `impl Into<String>`
///
/// Returns the new input type and a conversion to the original.
fn easier_string_assignment(field_ty: &Type, field_ident: &Ident) -> (TokenStream2, TokenStream2) {
if let Type::Path(type_path) = field_ty {
if let Some(last_segment) = type_path.path.segments.last() {
// Check if this type is a `String`
// Based on https://stackoverflow.com/questions/66906261/rust-proc-macro-derive-how-do-i-check-if-a-field-is-of-a-primitive-type-like-b
if last_segment.ident == Ident::new("String", last_segment.ident.span()) {
return (
quote::quote_spanned!(type_path.span() => impl Into<String>),
quote::quote_spanned!(field_ident.span() => #field_ident.into()),
);
}
}
}
(quote::quote_spanned!(field_ty.span() => #field_ty), quote::quote_spanned!(field_ident.span() => #field_ident))
}
/// Extract the identifier of the field (which should always be present)
fn extract_ident(field: &Field) -> syn::Result<&Ident> {
field
.ident
.as_ref()
.ok_or_else(|| syn::Error::new_spanned(field, "Constructing a builder not supported for unnamed fields"))
}
/// Find the type passed into the builder and the right hand side of the assignment.
///
/// Applies special behavior for easier String and WidgetCallback assignment.
fn find_type_and_assignment(field: &Field) -> syn::Result<(TokenStream2, TokenStream2)> {
let field_ty = &field.ty;
let field_ident = extract_ident(field)?;
let (mut function_input_ty, mut assignment) = easier_string_assignment(field_ty, field_ident);
// Check if type is `WidgetCallback`
if let Type::Path(type_path) = field_ty {
if let Some(last_segment) = type_path.path.segments.last() {
if let PathArguments::AngleBracketed(generic_args) = &last_segment.arguments {
if let Some(first_generic) = generic_args.args.first() {
if last_segment.ident == Ident::new("WidgetCallback", last_segment.ident.span()) {
// Assign builder pattern to assign the closure directly
function_input_ty = quote::quote_spanned!(field_ty.span() => impl Fn(&#first_generic) -> crate::messages::message::Message + 'static + Send + Sync);
assignment = quote::quote_spanned!(field_ident.span() => crate::messages::layout::utility_types::layout_widget::WidgetCallback::new(#field_ident));
}
}
}
}
}
Ok((function_input_ty, assignment))
}
// Construct a builder function for a specific field in the struct
fn construct_builder(field: &Field) -> syn::Result<TokenStream2> {
// Check if this field should be skipped with `#[widget_builder(skip)]`
if has_attribute(&field.attrs, "skip") {
return Ok(Default::default());
}
let field_ident = extract_ident(field)?;
// Create a doc comment literal describing the behaviour of the function
let doc_comment = Literal::string(&format!("Set the `{field_ident}` field using a builder pattern."));
let (function_input_ty, assignment) = find_type_and_assignment(field)?;
// Create builder function
Ok(quote::quote_spanned!(field.span() =>
#[doc = #doc_comment]
pub fn #field_ident(mut self, #field_ident: #function_input_ty) -> Self{
self.#field_ident = #assignment;
self
}
))
}
pub fn derive_widget_builder_impl(input_item: TokenStream2) -> syn::Result<TokenStream2> {
let input = syn::parse2::<DeriveInput>(input_item)?;
let struct_name_ident = input.ident;
// Extract the struct fields
let fields = match &input.data {
Data::Enum(enum_data) => return Err(syn::Error::new_spanned(enum_data.enum_token, "Derive widget builder is not supported for enums")),
Data::Union(union_data) => return Err(syn::Error::new_spanned(union_data.union_token, "Derive widget builder is not supported for unions")),
Data::Struct(struct_data) => &struct_data.fields,
};
// Create functions based on each field
let builder_functions = fields.iter().map(construct_builder).collect::<Result<Vec<_>, _>>()?;
// Check if this should not have the `widget_holder()` function due to a `#[widget_builder(not_widget_holder)]` attribute
let widget_holder_fn = if !has_attribute(&input.attrs, "not_widget_holder") {
// A doc comment for the widget_holder function
let widget_holder_doc_comment = Literal::string(&format!("Wrap {struct_name_ident} as a WidgetHolder."));
// Construct the `widget_holder` function
quote::quote! {
#[doc = #widget_holder_doc_comment]
pub fn widget_holder(self) -> crate::messages::layout::utility_types::layout_widget::WidgetHolder{
crate::messages::layout::utility_types::layout_widget::WidgetHolder::new( crate::messages::layout::utility_types::layout_widget::Widget::#struct_name_ident(self))
}
}
} else {
quote::quote!()
};
// The new function takes any fields tagged with `#[widget_builder(constructor)]` as arguments.
let new_fn = {
// A doc comment for the new function
let new_doc_comment = Literal::string(&format!("Create a new {struct_name_ident}, based on default values."));
let is_constructor = |field: &Field| has_attribute(&field.attrs, "constructor");
let idents = fields.iter().filter(|field| is_constructor(field)).map(extract_ident).collect::<Result<Vec<_>, _>>()?;
let types_and_assignments = fields.iter().filter(|field| is_constructor(field)).map(find_type_and_assignment).collect::<Result<Vec<_>, _>>()?;
let (types, assignments): (Vec<_>, Vec<_>) = types_and_assignments.into_iter().unzip();
let construction = if idents.is_empty() {
quote::quote!(Default::default())
} else {
let default = (idents.len() != fields.len()).then_some(quote::quote!(..Default::default())).unwrap_or_default();
quote::quote! {
Self {
#(#idents: #assignments,)*
#default
}
}
};
quote::quote! {
#[doc = #new_doc_comment]
pub fn new(#(#idents: #types),*) -> Self {
#construction
}
}
};
// Construct the code block
Ok(quote::quote! {
impl #struct_name_ident {
#new_fn
#(#builder_functions)*
#widget_holder_fn
}
})
}