1use proc_macro::TokenStream;
237use quote::{format_ident, quote};
238use syn::{
239 Data, DeriveInput, Error, Expr, ExprLit, Field, Fields, FieldsUnnamed, Index, Lit, Meta,
240 MetaNameValue, Result, parse_macro_input,
241};
242
243#[proc_macro_derive(Dissolve, attributes(dissolve, dissolved))]
253pub fn derive_dissolve(input: TokenStream) -> TokenStream {
254 let input = parse_macro_input!(input as DeriveInput);
255
256 match generate_dissolve_impl(&input) {
257 Ok(tokens) => tokens.into(),
258 Err(err) => err.to_compile_error().into(),
259 }
260}
261
262#[derive(Debug, Clone)]
263struct ContainerAttributes {
264 visibility: syn::Visibility,
265}
266
267impl ContainerAttributes {
268 const IDENT: &str = "dissolve";
269
270 const VISIBILITY_IDENT: &str = "visibility";
271
272 fn from_derive_input(input: &DeriveInput) -> Result<Self> {
273 let mut visibility = syn::parse_str::<syn::Visibility>("pub").unwrap();
274
275 for attr in input.attrs.iter().filter(|attr| attr.path().is_ident(Self::IDENT)) {
276 match &attr.meta {
277 Meta::List(_) => {
278 let nested_metas = attr.parse_args_with(
279 syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated,
280 )?;
281
282 for nested_meta in nested_metas {
283 match &nested_meta {
284 Meta::NameValue(MetaNameValue { path, value, .. }) => {
285 if path.is_ident(Self::VISIBILITY_IDENT) {
286 match value {
287 Expr::Lit(ExprLit { lit: Lit::Str(lit_str), .. }) => {
288 let vis_str = lit_str.value();
289 visibility = syn::parse_str::<syn::Visibility>(&vis_str)
290 .map_err(|e| {
291 Error::new_spanned(
292 value,
293 format!(
294 "invalid visibility: {e}. Supported: 'pub', 'pub(crate)', 'pub(super)', 'pub(self)' or empty for private",
295 ),
296 )
297 })?;
298 },
299 _ => {
300 return Err(Error::new_spanned(
301 value,
302 "visibility value must be a string literal",
303 ));
304 },
305 }
306 } else {
307 return Err(Error::new_spanned(
308 path,
309 format!(
310 "unknown dissolve attribute option '{}'; supported option: {}",
311 path.get_ident()
312 .map(|i| i.to_string())
313 .unwrap_or_default(),
314 Self::VISIBILITY_IDENT,
315 ),
316 ));
317 }
318 },
319 _ => {
320 return Err(Error::new_spanned(
321 nested_meta,
322 "dissolve container attribute must use name-value syntax: #[dissolve(visibility = \"...\")]",
323 ));
324 },
325 }
326 }
327 },
328 _ => {
329 return Err(Error::new_spanned(
330 attr,
331 "dissolve attribute must use list syntax: #[dissolve(visibility = \"...\")]",
332 ));
333 },
334 }
335 }
336
337 Ok(Self { visibility })
338 }
339}
340
341#[derive(Debug, Clone, PartialEq, Eq)]
342enum DissolvedOption {
343 Skip,
344 Rename(syn::Ident),
345}
346
347#[derive(Debug, Clone)]
348struct FieldInfo {
349 should_skip: bool,
350 renamed_to: Option<syn::Ident>,
351}
352
353impl DissolvedOption {
354 const IDENT: &str = "dissolved";
355
356 const SKIP_IDENT: &str = "skip";
357
358 const RENAME_IDENT: &str = "rename";
359
360 fn from_meta(meta: &Meta) -> Result<Self> {
361 let unknown_attribute_err = |path: &syn::Path| {
362 let path_str = path
363 .segments
364 .iter()
365 .map(|seg| seg.ident.to_string())
366 .collect::<Vec<_>>()
367 .join("::");
368
369 Error::new_spanned(
370 path,
371 format!(
372 "unknown dissolved attribute option '{}'; supported options: {}, {} = \"new_name\"",
373 Self::SKIP_IDENT,
374 Self::RENAME_IDENT,
375 path_str,
376 ),
377 )
378 };
379
380 let opt = match meta {
381 Meta::Path(path) => {
382 if !path.is_ident(Self::SKIP_IDENT) {
383 return Err(unknown_attribute_err(path));
384 }
385
386 DissolvedOption::Skip
387 },
388 Meta::NameValue(MetaNameValue { path, value, .. }) => {
389 if !path.is_ident(Self::RENAME_IDENT) {
390 return Err(unknown_attribute_err(path));
391 }
392
393 match value {
394 Expr::Lit(ExprLit { lit: Lit::Str(lit_str), .. }) => {
395 syn::parse_str::<syn::Ident>(&lit_str.value())
396 .map(DissolvedOption::Rename)?
397 },
398 _ => {
399 return Err(Error::new_spanned(
400 value,
401 format!("{} value must be a string literal", Self::RENAME_IDENT),
402 ));
403 },
404 }
405 },
406 Meta::List(_) => {
407 return Err(Error::new_spanned(
408 meta,
409 "nested lists are not supported in dissolved attributes",
410 ));
411 },
412 };
413
414 Ok(opt)
415 }
416}
417
418impl FieldInfo {
419 fn new() -> Self {
420 Self { should_skip: false, renamed_to: None }
421 }
422}
423
424fn generate_dissolve_impl(input: &DeriveInput) -> Result<proc_macro2::TokenStream> {
425 let struct_name = &input.ident;
426 let generics = &input.generics;
427 let container_attrs = ContainerAttributes::from_derive_input(input)?;
428
429 let Data::Struct(data_struct) = &input.data else {
430 return Err(Error::new_spanned(
431 input,
432 "Dissolve can only be derived for structs",
433 ));
434 };
435
436 match &data_struct.fields {
437 Fields::Named(fields) => {
438 generate_named_struct_impl(struct_name, generics, fields, &container_attrs)
439 },
440 Fields::Unnamed(fields) => {
441 generate_tuple_struct_impl(struct_name, generics, fields, &container_attrs)
442 },
443 Fields::Unit => Err(Error::new_spanned(
444 input,
445 "Dissolve cannot be derived for unit structs",
446 )),
447 }
448}
449
450fn generate_named_struct_impl(
451 struct_name: &syn::Ident,
452 generics: &syn::Generics,
453 fields: &syn::FieldsNamed,
454 container_attrs: &ContainerAttributes,
455) -> Result<proc_macro2::TokenStream> {
456 let included_fields: Vec<_> = fields
457 .named
458 .iter()
459 .map(|field| {
460 let info = get_field_info(field)?;
461 if info.should_skip {
462 Ok((None, info))
463 } else {
464 Ok((Some(field), info))
465 }
466 })
467 .filter_map(|res| match res {
468 Ok((Some(field), info)) => Some(Ok((field, info))),
469 Err(e) => Some(Err(e)),
470 _ => None,
471 })
472 .collect::<Result<_>>()?;
473
474 if included_fields.is_empty() {
475 return Err(Error::new_spanned(
476 struct_name,
477 "cannot create dissolved struct with no fields (all fields are skipped)",
478 ));
479 }
480
481 let field_definitions = included_fields.iter().map(|(field, info)| {
482 let original_name = field.ident.as_ref().unwrap();
484 let ty = &field.ty;
485
486 let dissolved_field_name = match &info.renamed_to {
487 Some(new_name) => new_name,
488 None => original_name,
489 };
490
491 let doc_attrs = field.attrs.iter().filter(|attr| attr.path().is_ident("doc"));
493
494 quote! {
495 #(#doc_attrs)*
496 pub #dissolved_field_name: #ty
497 }
498 });
499
500 let field_moves = included_fields.iter().map(|(field, info)| {
501 let original_name = field.ident.as_ref().unwrap();
503
504 let dissolved_field_name = match &info.renamed_to {
505 Some(new_name) => new_name,
506 None => original_name,
507 };
508
509 quote! { #dissolved_field_name: self.#original_name }
510 });
511
512 let dissolved_struct_name = format_ident!("{}Dissolved", struct_name);
513
514 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
516
517 let dissolved_struct_doc = format!(
518 "Dissolved struct for [`{struct_name}`].\n\n\
519 This struct contains all non-skipped fields from the original struct with public visibility. \
520 Fields may be renamed according to `#[dissolved(rename = \"...\")]` attributes.",
521 );
522
523 let visibility = &container_attrs.visibility;
524
525 Ok(quote! {
526 #[doc = #dissolved_struct_doc]
527 pub struct #dissolved_struct_name #impl_generics #where_clause {
528 #(#field_definitions),*
529 }
530
531 impl #impl_generics #struct_name #ty_generics #where_clause {
532 #visibility fn dissolve(self) -> #dissolved_struct_name #ty_generics {
537 #dissolved_struct_name {
538 #(#field_moves),*
539 }
540 }
541 }
542 })
543}
544
545fn generate_tuple_struct_impl(
546 struct_name: &syn::Ident,
547 generics: &syn::Generics,
548 fields: &FieldsUnnamed,
549 container_attrs: &ContainerAttributes,
550) -> Result<proc_macro2::TokenStream> {
551 let included_fields: Vec<_> = fields
553 .unnamed
554 .iter()
555 .enumerate()
556 .filter_map(|(index, field)| {
557 match get_field_info(field) {
558 Ok(info) => {
559 if info.should_skip {
560 None
561 } else {
562 if info.renamed_to.is_some() {
564 Some(Err(Error::new_spanned(
565 field,
566 format!(
567 "{} is unsupported for tuple struct fields, only {} is allowed",
568 DissolvedOption::RENAME_IDENT,
569 DissolvedOption::SKIP_IDENT,
570 ),
571 )))
572 } else {
573 Some(Ok((index, field)))
574 }
575 }
576 },
577 Err(err) => Some(Err(err)),
578 }
579 })
580 .collect::<Result<_>>()?;
581
582 if included_fields.is_empty() {
583 return Err(Error::new_spanned(
584 struct_name,
585 "cannot create dissolved tuple with no fields (all fields are skipped)",
586 ));
587 }
588
589 let tuple_types = included_fields.iter().map(|(_, field)| &field.ty);
590 let tuple_type = if included_fields.len() == 1 {
591 let ty = &included_fields[0].1.ty;
593 quote! { (#ty,) }
594 } else {
595 quote! { (#(#tuple_types),*) }
596 };
597
598 let field_moves = included_fields.iter().map(|(original_index, _)| {
599 let index = Index::from(*original_index);
600 quote! { self.#index }
601 });
602
603 let tuple_construction = if included_fields.len() == 1 {
604 quote! { (#(#field_moves,)*) }
606 } else {
607 quote! { (#(#field_moves),*) }
608 };
609
610 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
612 let visibility = &container_attrs.visibility;
613
614 Ok(quote! {
615 impl #impl_generics #struct_name #ty_generics #where_clause {
616 #visibility fn dissolve(self) -> #tuple_type {
618 #tuple_construction
619 }
620 }
621 })
622}
623
624fn get_field_info(field: &Field) -> Result<FieldInfo> {
625 let mut field_info = FieldInfo::new();
626
627 for attr in field.attrs.iter().filter(|attr| attr.path().is_ident(DissolvedOption::IDENT)) {
628 match attr.meta.clone() {
629 Meta::List(_) => {
630 let nested_metas = attr.parse_args_with(
632 syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated,
633 )?;
634
635 for nested_meta in nested_metas {
636 let option = DissolvedOption::from_meta(&nested_meta)?;
637 match option {
638 DissolvedOption::Skip => {
639 if field_info.renamed_to.is_some() {
640 return Err(Error::new_spanned(
641 attr,
642 format!(
643 "cannot use {} on skipped field",
644 DissolvedOption::RENAME_IDENT,
645 ),
646 ));
647 }
648
649 field_info.should_skip = true;
650 },
651 DissolvedOption::Rename(new_ident) => {
652 if field_info.should_skip {
653 return Err(Error::new_spanned(
654 attr,
655 format!(
656 "cannot use {} on skipped field",
657 DissolvedOption::RENAME_IDENT,
658 ),
659 ));
660 }
661
662 if field_info.renamed_to.is_some() {
663 return Err(Error::new_spanned(
664 attr,
665 format!(
666 "cannot specify multiple {} options on the same field",
667 DissolvedOption::RENAME_IDENT,
668 ),
669 ));
670 }
671
672 field_info.renamed_to = Some(new_ident);
673 },
674 }
675 }
676 },
677 Meta::Path(_) => {
678 return Err(Error::new_spanned(
679 attr,
680 format!(
681 "dissolved attribute requires options, use #[dissolved({})] or #[dissolved({} = \"new_name\")] instead",
682 DissolvedOption::SKIP_IDENT,
683 DissolvedOption::RENAME_IDENT,
684 ),
685 ));
686 },
687 Meta::NameValue(_) => {
688 return Err(Error::new_spanned(
689 attr,
690 format!(
691 "dissolved attribute should use list syntax: #[dissolved({} = \"new_name\")] instead of #[dissolved = ...]",
692 DissolvedOption::RENAME_IDENT,
693 ),
694 ));
695 },
696 }
697 }
698
699 Ok(field_info)
700}