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 quote! { pub #dissolved_field_name: #ty }
172 });
173
174 let field_moves = included_fields.iter().map(|(field, info)| {
175 let original_name = field.ident.as_ref().unwrap();
177
178 let dissolved_field_name = match &info.renamed_to {
179 Some(new_name) => new_name,
180 None => original_name,
181 };
182
183 quote! { #dissolved_field_name: self.#original_name }
184 });
185
186 let dissolved_struct_name = format_ident!("{}Dissolved", struct_name);
187
188 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
190
191 Ok(quote! {
192 pub struct #dissolved_struct_name #impl_generics #where_clause {
193 #(#field_definitions),*
194 }
195
196 impl #impl_generics #struct_name #ty_generics #where_clause {
197 pub fn dissolve(self) -> #dissolved_struct_name #ty_generics {
202 #dissolved_struct_name {
203 #(#field_moves),*
204 }
205 }
206 }
207 })
208}
209
210fn generate_tuple_struct_impl(
211 struct_name: &syn::Ident,
212 generics: &syn::Generics,
213 fields: &FieldsUnnamed,
214) -> Result<proc_macro2::TokenStream> {
215 let included_fields: Vec<_> = fields
217 .unnamed
218 .iter()
219 .enumerate()
220 .filter_map(|(index, field)| {
221 match get_field_info(field) {
222 Ok(info) => {
223 if info.should_skip {
224 None
225 } else {
226 if info.renamed_to.is_some() {
228 Some(Err(Error::new_spanned(
229 field,
230 format!(
231 "{} is unsupported for tuple struct fields, only {} is allowed",
232 DissolvedOption::RENAME_IDENT,
233 DissolvedOption::SKIP_IDENT,
234 ),
235 )))
236 } else {
237 Some(Ok((index, field)))
238 }
239 }
240 },
241 Err(err) => Some(Err(err)),
242 }
243 })
244 .collect::<Result<_>>()?;
245
246 if included_fields.is_empty() {
247 return Err(Error::new_spanned(
248 struct_name,
249 "cannot create dissolved tuple with no fields (all fields are skipped)",
250 ));
251 }
252
253 let tuple_types = included_fields.iter().map(|(_, field)| &field.ty);
254 let tuple_type = if included_fields.len() == 1 {
255 let ty = &included_fields[0].1.ty;
257 quote! { (#ty,) }
258 } else {
259 quote! { (#(#tuple_types),*) }
260 };
261
262 let field_moves = included_fields.iter().map(|(original_index, _)| {
263 let index = Index::from(*original_index);
264 quote! { self.#index }
265 });
266
267 let tuple_construction = if included_fields.len() == 1 {
268 quote! { (#(#field_moves,)*) }
270 } else {
271 quote! { (#(#field_moves),*) }
272 };
273
274 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
276
277 Ok(quote! {
278 impl #impl_generics #struct_name #ty_generics #where_clause {
279 pub fn dissolve(self) -> #tuple_type {
281 #tuple_construction
282 }
283 }
284 })
285}
286
287fn get_field_info(field: &Field) -> Result<FieldInfo> {
288 let mut field_info = FieldInfo::new();
289
290 for attr in field.attrs.iter().filter(|attr| attr.path().is_ident(DissolvedOption::IDENT)) {
291 match attr.meta.clone() {
292 Meta::List(_) => {
293 let nested_metas = attr.parse_args_with(
295 syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated,
296 )?;
297
298 for nested_meta in nested_metas {
299 let option = DissolvedOption::from_meta(&nested_meta)?;
300 match option {
301 DissolvedOption::Skip => {
302 if field_info.renamed_to.is_some() {
303 return Err(Error::new_spanned(
304 attr,
305 format!(
306 "cannot use {} on skipped field",
307 DissolvedOption::RENAME_IDENT,
308 ),
309 ));
310 }
311
312 field_info.should_skip = true;
313 },
314 DissolvedOption::Rename(new_ident) => {
315 if field_info.should_skip {
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 if field_info.renamed_to.is_some() {
326 return Err(Error::new_spanned(
327 attr,
328 format!(
329 "cannot specify multiple {} options on the same field",
330 DissolvedOption::RENAME_IDENT,
331 ),
332 ));
333 }
334
335 field_info.renamed_to = Some(new_ident);
336 },
337 }
338 }
339 },
340 Meta::Path(_) => {
341 return Err(Error::new_spanned(
342 attr,
343 format!(
344 "dissolved attribute requires options, use #[dissolved({})] or #[dissolved({} = \"new_name\")] instead",
345 DissolvedOption::SKIP_IDENT,
346 DissolvedOption::RENAME_IDENT,
347 ),
348 ));
349 },
350 Meta::NameValue(_) => {
351 return Err(Error::new_spanned(
352 attr,
353 format!(
354 "dissolved attribute should use list syntax: #[dissolved({} = \"new_name\")] instead of #[dissolved = ...]",
355 DissolvedOption::RENAME_IDENT,
356 ),
357 ));
358 },
359 }
360 }
361
362 Ok(field_info)
363}