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
113 let Data::Struct(data_struct) = &input.data else {
114 return Err(Error::new_spanned(
115 input,
116 "Dissolve can only be derived for structs",
117 ));
118 };
119
120 match &data_struct.fields {
121 Fields::Named(fields) => generate_named_struct_impl(struct_name, fields),
122 Fields::Unnamed(fields) => generate_tuple_struct_impl(struct_name, fields),
123 Fields::Unit => Err(Error::new_spanned(
124 input,
125 "Dissolve cannot be derived for unit structs",
126 )),
127 }
128}
129
130fn generate_named_struct_impl(
131 struct_name: &syn::Ident,
132 fields: &syn::FieldsNamed,
133) -> Result<proc_macro2::TokenStream> {
134 let included_fields: Vec<_> = fields
135 .named
136 .iter()
137 .map(|field| {
138 let info = get_field_info(field)?;
139 if info.should_skip {
140 Ok((None, info))
141 } else {
142 Ok((Some(field), info))
143 }
144 })
145 .filter_map(|res| match res {
146 Ok((Some(field), info)) => Some(Ok((field, info))),
147 Err(e) => Some(Err(e)),
148 _ => None,
149 })
150 .collect::<Result<_>>()?;
151
152 if included_fields.is_empty() {
153 return Err(Error::new_spanned(
154 struct_name,
155 "cannot create dissolved struct with no fields (all fields are skipped)",
156 ));
157 }
158
159 let field_definitions = included_fields.iter().map(|(field, info)| {
160 let original_name = field.ident.as_ref().unwrap();
162 let ty = &field.ty;
163
164 let dissolved_field_name = match &info.renamed_to {
165 Some(new_name) => new_name,
166 None => original_name,
167 };
168
169 quote! { pub #dissolved_field_name: #ty }
170 });
171
172 let field_moves = included_fields.iter().map(|(field, info)| {
173 let original_name = field.ident.as_ref().unwrap();
175
176 let dissolved_field_name = match &info.renamed_to {
177 Some(new_name) => new_name,
178 None => original_name,
179 };
180
181 quote! { #dissolved_field_name: self.#original_name }
182 });
183
184 let dissolved_struct_name = format_ident!("{}Dissolved", struct_name);
185
186 Ok(quote! {
187 pub struct #dissolved_struct_name {
188 #(#field_definitions),*
189 }
190
191 impl #struct_name {
192 pub fn dissolve(self) -> #dissolved_struct_name {
197 #dissolved_struct_name {
198 #(#field_moves),*
199 }
200 }
201 }
202 })
203}
204
205fn generate_tuple_struct_impl(
206 struct_name: &syn::Ident,
207 fields: &FieldsUnnamed,
208) -> Result<proc_macro2::TokenStream> {
209 let included_fields: Vec<_> = fields
211 .unnamed
212 .iter()
213 .enumerate()
214 .filter_map(|(index, field)| {
215 match get_field_info(field) {
216 Ok(info) => {
217 if info.should_skip {
218 None
219 } else {
220 if info.renamed_to.is_some() {
222 Some(Err(Error::new_spanned(
223 field,
224 format!(
225 "{} is unsupported for tuple struct fields, only {} is allowed",
226 DissolvedOption::RENAME_IDENT,
227 DissolvedOption::SKIP_IDENT,
228 ),
229 )))
230 } else {
231 Some(Ok((index, field)))
232 }
233 }
234 },
235 Err(err) => Some(Err(err)),
236 }
237 })
238 .collect::<Result<_>>()?;
239
240 if included_fields.is_empty() {
241 return Err(Error::new_spanned(
242 struct_name,
243 "cannot create dissolved tuple with no fields (all fields are skipped)",
244 ));
245 }
246
247 let tuple_types = included_fields.iter().map(|(_, field)| &field.ty);
248 let tuple_type = if included_fields.len() == 1 {
249 let ty = &included_fields[0].1.ty;
251 quote! { (#ty,) }
252 } else {
253 quote! { (#(#tuple_types),*) }
254 };
255
256 let field_moves = included_fields.iter().map(|(original_index, _)| {
257 let index = Index::from(*original_index);
258 quote! { self.#index }
259 });
260
261 let tuple_construction = if included_fields.len() == 1 {
262 quote! { (#(#field_moves,)*) }
264 } else {
265 quote! { (#(#field_moves),*) }
266 };
267
268 Ok(quote! {
269 impl #struct_name {
270 pub fn dissolve(self) -> #tuple_type {
272 #tuple_construction
273 }
274 }
275 })
276}
277
278fn get_field_info(field: &Field) -> Result<FieldInfo> {
279 let mut field_info = FieldInfo::new();
280
281 for attr in field.attrs.iter().filter(|attr| attr.path().is_ident(DissolvedOption::IDENT)) {
282 match attr.meta.clone() {
283 Meta::List(_) => {
284 let nested_metas = attr.parse_args_with(
286 syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated,
287 )?;
288
289 for nested_meta in nested_metas {
290 let option = DissolvedOption::from_meta(&nested_meta)?;
291 match option {
292 DissolvedOption::Skip => {
293 if field_info.renamed_to.is_some() {
294 return Err(Error::new_spanned(
295 attr,
296 format!(
297 "cannot use {} on skipped field",
298 DissolvedOption::RENAME_IDENT,
299 ),
300 ));
301 }
302
303 field_info.should_skip = true;
304 },
305 DissolvedOption::Rename(new_ident) => {
306 if field_info.should_skip {
307 return Err(Error::new_spanned(
308 attr,
309 format!(
310 "cannot use {} on skipped field",
311 DissolvedOption::RENAME_IDENT,
312 ),
313 ));
314 }
315
316 if field_info.renamed_to.is_some() {
317 return Err(Error::new_spanned(
318 attr,
319 format!(
320 "cannot specify multiple {} options on the same field",
321 DissolvedOption::RENAME_IDENT,
322 ),
323 ));
324 }
325
326 field_info.renamed_to = Some(new_ident);
327 },
328 }
329 }
330 },
331 Meta::Path(_) => {
332 return Err(Error::new_spanned(
333 attr,
334 format!(
335 "dissolved attribute requires options, use #[dissolved({})] or #[dissolved({} = \"new_name\")] instead",
336 DissolvedOption::SKIP_IDENT,
337 DissolvedOption::RENAME_IDENT,
338 ),
339 ));
340 },
341 Meta::NameValue(_) => {
342 return Err(Error::new_spanned(
343 attr,
344 format!(
345 "dissolved attribute should use list syntax: #[dissolved({} = \"new_name\")] instead of #[dissolved = ...]",
346 DissolvedOption::RENAME_IDENT,
347 ),
348 ));
349 },
350 }
351 }
352
353 Ok(field_info)
354}