basecoat_core_macros/
lib.rs1use proc_macro::TokenStream;
2use proc_macro2::TokenStream as TokenStream2;
3use quote::{format_ident, quote};
4use syn::{
5 Data, DeriveInput, Field, Fields, Ident, LitStr, Meta, Type, parse_macro_input,
6 punctuated::Punctuated, token::Comma,
7};
8
9#[proc_macro_derive(BasecoatProps, attributes(prop))]
33pub fn derive_basecoat_props(input: TokenStream) -> TokenStream {
34 let input = parse_macro_input!(input as DeriveInput);
35 match derive_impl(input) {
36 Ok(ts) => ts.into(),
37 Err(e) => e.to_compile_error().into(),
38 }
39}
40
41struct PropField {
44 ident: Ident,
45 ty: Type,
46 default_expr: Option<TokenStream2>, into: bool,
48 extend: bool,
49}
50
51impl PropField {
52 fn from_field(field: &Field) -> syn::Result<Self> {
53 let ident = field
54 .ident
55 .clone()
56 .ok_or_else(|| syn::Error::new_spanned(field, "BasecoatProps requires named fields"))?;
57 let ty = field.ty.clone();
58
59 let mut default_expr: Option<TokenStream2> = None;
60 let mut into = false;
61 let mut extend = false;
62 let mut has_default_marker = false;
63
64 for attr in &field.attrs {
65 if !attr.path().is_ident("prop") {
66 continue;
67 }
68 let nested = attr.parse_args_with(Punctuated::<Meta, Comma>::parse_terminated)?;
69 for meta in nested {
70 match &meta {
71 Meta::Path(p) if p.is_ident("default") => {
72 has_default_marker = true;
73 default_expr = Some(quote! { ::core::default::Default::default() });
74 }
75 Meta::NameValue(nv) if nv.path.is_ident("default") => {
76 has_default_marker = true;
77 let val = &nv.value;
78 default_expr = Some(quote! { #val });
79 }
80 Meta::Path(p) if p.is_ident("into") => {
81 into = true;
82 }
83 Meta::Path(p) if p.is_ident("optional") => {
84 if !has_default_marker {
85 default_expr = Some(quote! { ::core::option::Option::None });
86 }
87 }
88 Meta::Path(p) if p.is_ident("extend") => {
89 extend = true;
90 if !has_default_marker {
91 default_expr = Some(quote! { ::core::default::Default::default() });
92 }
93 }
94 other => {
95 return Err(syn::Error::new_spanned(
96 other,
97 "unknown prop attribute; expected default, into, optional, extend",
98 ));
99 }
100 }
101 }
102 }
103
104 Ok(PropField {
105 ident,
106 ty,
107 default_expr,
108 into,
109 extend,
110 })
111 }
112}
113
114fn derive_impl(input: DeriveInput) -> syn::Result<TokenStream2> {
117 let struct_ident = &input.ident;
118 let builder_ident = format_ident!("{}Builder", struct_ident);
119
120 let fields = match &input.data {
121 Data::Struct(ds) => match &ds.fields {
122 Fields::Named(f) => &f.named,
123 _ => {
124 return Err(syn::Error::new_spanned(
125 &input.ident,
126 "BasecoatProps only supports structs with named fields",
127 ));
128 }
129 },
130 _ => {
131 return Err(syn::Error::new_spanned(
132 &input.ident,
133 "BasecoatProps can only be derived on structs",
134 ));
135 }
136 };
137
138 let prop_fields: Vec<PropField> = fields
139 .iter()
140 .map(PropField::from_field)
141 .collect::<syn::Result<_>>()?;
142
143 let extend_fields: Vec<_> = prop_fields.iter().filter(|f| f.extend).collect();
145 if extend_fields.len() > 1 {
146 return Err(syn::Error::new_spanned(
147 struct_ident,
148 "at most one #[prop(extend)] field is allowed per prop struct",
149 ));
150 }
151 let extend_field_name: Option<LitStr> = extend_fields
152 .first()
153 .map(|f| LitStr::new(&f.ident.to_string(), proc_macro2::Span::call_site()));
154
155 let builder_field_decls: Vec<TokenStream2> = prop_fields
159 .iter()
160 .map(|f| {
161 let ident = &f.ident;
162 let ty = &f.ty;
163 quote! { #ident: ::core::option::Option<#ty> }
164 })
165 .collect();
166
167 let builder_none_inits: Vec<TokenStream2> = prop_fields
169 .iter()
170 .map(|f| {
171 let ident = &f.ident;
172 quote! { #ident: ::core::option::Option::None }
173 })
174 .collect();
175
176 let setters: Vec<TokenStream2> = prop_fields
178 .iter()
179 .map(|f| {
180 let ident = &f.ident;
181 let ty = &f.ty;
182 if f.into {
183 quote! {
184 pub fn #ident(mut self, val: impl ::core::convert::Into<#ty>) -> Self {
185 self.#ident = ::core::option::Option::Some(val.into());
186 self
187 }
188 }
189 } else {
190 quote! {
191 pub fn #ident(mut self, val: #ty) -> Self {
192 self.#ident = ::core::option::Option::Some(val);
193 self
194 }
195 }
196 }
197 })
198 .collect();
199
200 let struct_name_str = struct_ident.to_string();
202 let build_fields: Vec<TokenStream2> = prop_fields
203 .iter()
204 .map(|f| {
205 let ident = &f.ident;
206 let field_name_str = ident.to_string();
207 if let Some(default) = &f.default_expr {
208 quote! {
209 #ident: self.#ident.unwrap_or_else(|| #default)
210 }
211 } else {
212 quote! {
213 #ident: self.#ident.unwrap_or_else(|| {
214 panic!(
215 "required field `{}` was not set on `{}::builder()`",
216 #field_name_str,
217 #struct_name_str,
218 )
219 })
220 }
221 }
222 })
223 .collect();
224
225 let extend_const = if let Some(name) = &extend_field_name {
227 quote! {
228 pub const __BASECOAT_EXTEND_FIELD: ::core::option::Option<&'static str> =
229 ::core::option::Option::Some(#name);
230 }
231 } else {
232 quote! {
233 pub const __BASECOAT_EXTEND_FIELD: ::core::option::Option<&'static str> =
234 ::core::option::Option::None;
235 }
236 };
237
238 let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
239
240 Ok(quote! {
241 impl #impl_generics #struct_ident #ty_generics #where_clause {
242 pub fn builder() -> #builder_ident #ty_generics {
244 #builder_ident {
245 #( #builder_none_inits, )*
246 }
247 }
248
249 #extend_const
250 }
251
252 pub struct #builder_ident #ty_generics #where_clause {
253 #( #builder_field_decls, )*
254 }
255
256 impl #impl_generics #builder_ident #ty_generics #where_clause {
257 #( #setters )*
258
259 pub fn build(self) -> #struct_ident #ty_generics {
260 #struct_ident {
261 #( #build_fields, )*
262 }
263 }
264 }
265
266 impl #impl_generics ::core::convert::From<#builder_ident #ty_generics>
267 for #struct_ident #ty_generics #where_clause
268 {
269 fn from(b: #builder_ident #ty_generics) -> Self {
270 b.build()
271 }
272 }
273 })
274}