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