1use proc_macro::TokenStream;
2use quote::{format_ident, quote};
3use syn::{
4 Data, DeriveInput, Error, Expr, ExprLit, Field, Fields, FieldsUnnamed, Index, Lit, Meta,
5 MetaNameValue, Result, parse_macro_input,
6};
7
8#[proc_macro_derive(Dissolve, attributes(dissolved))]
18pub fn derive_dissolve(input: TokenStream) -> TokenStream {
19 let input = parse_macro_input!(input as DeriveInput);
20
21 match generate_dissolve_impl(&input) {
22 Ok(tokens) => tokens.into(),
23 Err(err) => err.to_compile_error().into(),
24 }
25}
26
27#[derive(Debug, Clone, PartialEq, Eq)]
28enum DissolvedOption {
29 Skip,
30 Rename(syn::Ident),
31}
32
33#[derive(Debug, Clone)]
34struct FieldInfo {
35 should_skip: bool,
36 renamed_to: Option<syn::Ident>,
37}
38
39impl DissolvedOption {
40 const IDENT: &str = "dissolved";
41
42 const SKIP_IDENT: &str = "skip";
43
44 const RENAME_IDENT: &str = "rename";
45
46 fn from_meta(meta: &Meta) -> Result<Self> {
47 let unknown_attribute_err = |path: &syn::Path| {
48 let path_str = path
49 .segments
50 .iter()
51 .map(|seg| seg.ident.to_string())
52 .collect::<Vec<_>>()
53 .join("::");
54
55 Error::new_spanned(
56 path,
57 format!(
58 "unknown dissolved attribute option '{}'; supported options: {}, {} = \"new_name\"",
59 Self::SKIP_IDENT,
60 Self::RENAME_IDENT,
61 path_str,
62 ),
63 )
64 };
65
66 let opt = match meta {
67 Meta::Path(path) => {
68 if !path.is_ident(Self::SKIP_IDENT) {
69 return Err(unknown_attribute_err(path));
70 }
71
72 DissolvedOption::Skip
73 },
74 Meta::NameValue(MetaNameValue { path, value, .. }) => {
75 if !path.is_ident(Self::RENAME_IDENT) {
76 return Err(unknown_attribute_err(path));
77 }
78
79 match value {
80 Expr::Lit(ExprLit { lit: Lit::Str(lit_str), .. }) => {
81 syn::parse_str::<syn::Ident>(&lit_str.value())
82 .map(DissolvedOption::Rename)?
83 },
84 _ => {
85 return Err(Error::new_spanned(
86 value,
87 format!("{} value must be a string literal", Self::RENAME_IDENT),
88 ));
89 },
90 }
91 },
92 Meta::List(_) => {
93 return Err(Error::new_spanned(
94 meta,
95 "nested lists are not supported in dissolved attributes",
96 ));
97 },
98 };
99
100 Ok(opt)
101 }
102}
103
104impl FieldInfo {
105 fn new() -> Self {
106 Self { should_skip: false, renamed_to: None }
107 }
108}
109
110fn generate_dissolve_impl(input: &DeriveInput) -> Result<proc_macro2::TokenStream> {
111 let struct_name = &input.ident;
112 let generics = &input.generics;
113
114 let Data::Struct(data_struct) = &input.data else {
115 return Err(Error::new_spanned(
116 input,
117 "Dissolve can only be derived for structs",
118 ));
119 };
120
121 match &data_struct.fields {
122 Fields::Named(fields) => generate_named_struct_impl(struct_name, generics, fields),
123 Fields::Unnamed(fields) => generate_tuple_struct_impl(struct_name, generics, fields),
124 Fields::Unit => Err(Error::new_spanned(
125 input,
126 "Dissolve cannot be derived for unit structs",
127 )),
128 }
129}
130
131fn generate_named_struct_impl(
132 struct_name: &syn::Ident,
133 generics: &syn::Generics,
134 fields: &syn::FieldsNamed,
135) -> Result<proc_macro2::TokenStream> {
136 let included_fields: Vec<_> = fields
137 .named
138 .iter()
139 .map(|field| {
140 let info = get_field_info(field)?;
141 if info.should_skip {
142 Ok((None, info))
143 } else {
144 Ok((Some(field), info))
145 }
146 })
147 .filter_map(|res| match res {
148 Ok((Some(field), info)) => Some(Ok((field, info))),
149 Err(e) => Some(Err(e)),
150 _ => None,
151 })
152 .collect::<Result<_>>()?;
153
154 if included_fields.is_empty() {
155 return Err(Error::new_spanned(
156 struct_name,
157 "cannot create dissolved struct with no fields (all fields are skipped)",
158 ));
159 }
160
161 let field_definitions = included_fields.iter().map(|(field, info)| {
162 let original_name = field.ident.as_ref().unwrap();
164 let ty = &field.ty;
165
166 let dissolved_field_name = match &info.renamed_to {
167 Some(new_name) => new_name,
168 None => original_name,
169 };
170
171 let doc_attrs = field.attrs.iter().filter(|attr| attr.path().is_ident("doc"));
173
174 quote! {
175 #(#doc_attrs)*
176 pub #dissolved_field_name: #ty
177 }
178 });
179
180 let field_moves = included_fields.iter().map(|(field, info)| {
181 let original_name = field.ident.as_ref().unwrap();
183
184 let dissolved_field_name = match &info.renamed_to {
185 Some(new_name) => new_name,
186 None => original_name,
187 };
188
189 quote! { #dissolved_field_name: self.#original_name }
190 });
191
192 let dissolved_struct_name = format_ident!("{}Dissolved", struct_name);
193
194 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
196
197 let dissolved_struct_doc = format!(
198 "Dissolved struct for [`{struct_name}`].\n\n\
199 This struct contains all non-skipped fields from the original struct with public visibility. \
200 Fields may be renamed according to `#[dissolved(rename = \"...\")]` attributes.",
201 );
202
203 Ok(quote! {
204 #[doc = #dissolved_struct_doc]
205 pub struct #dissolved_struct_name #impl_generics #where_clause {
206 #(#field_definitions),*
207 }
208
209 impl #impl_generics #struct_name #ty_generics #where_clause {
210 pub fn dissolve(self) -> #dissolved_struct_name #ty_generics {
215 #dissolved_struct_name {
216 #(#field_moves),*
217 }
218 }
219 }
220 })
221}
222
223fn generate_tuple_struct_impl(
224 struct_name: &syn::Ident,
225 generics: &syn::Generics,
226 fields: &FieldsUnnamed,
227) -> Result<proc_macro2::TokenStream> {
228 let included_fields: Vec<_> = fields
230 .unnamed
231 .iter()
232 .enumerate()
233 .filter_map(|(index, field)| {
234 match get_field_info(field) {
235 Ok(info) => {
236 if info.should_skip {
237 None
238 } else {
239 if info.renamed_to.is_some() {
241 Some(Err(Error::new_spanned(
242 field,
243 format!(
244 "{} is unsupported for tuple struct fields, only {} is allowed",
245 DissolvedOption::RENAME_IDENT,
246 DissolvedOption::SKIP_IDENT,
247 ),
248 )))
249 } else {
250 Some(Ok((index, field)))
251 }
252 }
253 },
254 Err(err) => Some(Err(err)),
255 }
256 })
257 .collect::<Result<_>>()?;
258
259 if included_fields.is_empty() {
260 return Err(Error::new_spanned(
261 struct_name,
262 "cannot create dissolved tuple with no fields (all fields are skipped)",
263 ));
264 }
265
266 let tuple_types = included_fields.iter().map(|(_, field)| &field.ty);
267 let tuple_type = if included_fields.len() == 1 {
268 let ty = &included_fields[0].1.ty;
270 quote! { (#ty,) }
271 } else {
272 quote! { (#(#tuple_types),*) }
273 };
274
275 let field_moves = included_fields.iter().map(|(original_index, _)| {
276 let index = Index::from(*original_index);
277 quote! { self.#index }
278 });
279
280 let tuple_construction = if included_fields.len() == 1 {
281 quote! { (#(#field_moves,)*) }
283 } else {
284 quote! { (#(#field_moves),*) }
285 };
286
287 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
289
290 Ok(quote! {
291 impl #impl_generics #struct_name #ty_generics #where_clause {
292 pub fn dissolve(self) -> #tuple_type {
294 #tuple_construction
295 }
296 }
297 })
298}
299
300fn get_field_info(field: &Field) -> Result<FieldInfo> {
301 let mut field_info = FieldInfo::new();
302
303 for attr in field.attrs.iter().filter(|attr| attr.path().is_ident(DissolvedOption::IDENT)) {
304 match attr.meta.clone() {
305 Meta::List(_) => {
306 let nested_metas = attr.parse_args_with(
308 syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated,
309 )?;
310
311 for nested_meta in nested_metas {
312 let option = DissolvedOption::from_meta(&nested_meta)?;
313 match option {
314 DissolvedOption::Skip => {
315 if field_info.renamed_to.is_some() {
316 return Err(Error::new_spanned(
317 attr,
318 format!(
319 "cannot use {} on skipped field",
320 DissolvedOption::RENAME_IDENT,
321 ),
322 ));
323 }
324
325 field_info.should_skip = true;
326 },
327 DissolvedOption::Rename(new_ident) => {
328 if field_info.should_skip {
329 return Err(Error::new_spanned(
330 attr,
331 format!(
332 "cannot use {} on skipped field",
333 DissolvedOption::RENAME_IDENT,
334 ),
335 ));
336 }
337
338 if field_info.renamed_to.is_some() {
339 return Err(Error::new_spanned(
340 attr,
341 format!(
342 "cannot specify multiple {} options on the same field",
343 DissolvedOption::RENAME_IDENT,
344 ),
345 ));
346 }
347
348 field_info.renamed_to = Some(new_ident);
349 },
350 }
351 }
352 },
353 Meta::Path(_) => {
354 return Err(Error::new_spanned(
355 attr,
356 format!(
357 "dissolved attribute requires options, use #[dissolved({})] or #[dissolved({} = \"new_name\")] instead",
358 DissolvedOption::SKIP_IDENT,
359 DissolvedOption::RENAME_IDENT,
360 ),
361 ));
362 },
363 Meta::NameValue(_) => {
364 return Err(Error::new_spanned(
365 attr,
366 format!(
367 "dissolved attribute should use list syntax: #[dissolved({} = \"new_name\")] instead of #[dissolved = ...]",
368 DissolvedOption::RENAME_IDENT,
369 ),
370 ));
371 },
372 }
373 }
374
375 Ok(field_info)
376}