fancy_constructor 2.1.0

Derive a highly configurable constructor for your struct
Documentation
use macroific::prelude::*;
use proc_macro2::Span;
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::spanned::Spanned;
use syn::{Data, DeriveInput, Error, Variant};

use crate::options::ContainerOptions;
use crate::types::FieldsSource;
use crate::{FancyConstructor, ATTR_NAME};

impl Parse for FancyConstructor {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let DeriveInput {
            ident: struct_name,
            attrs,
            generics,
            data,
            vis: _,
        } = input.parse()?;

        let fields = match data {
            Data::Struct(s) => FieldsSource::Struct(s.fields.try_into()?),
            Data::Enum(e) => {
                let variant = find_variant(&e.enum_token, e.variants)?;

                FieldsSource::Enum {
                    fields: variant.fields.try_into()?,
                    variant: variant.ident,
                }
            }
            Data::Union(u) => {
                return Err(Error::new_spanned(u.union_token, "Unions not supported"));
            }
        };

        Ok(Self {
            opts: if attrs.is_empty() {
                ContainerOptions::default()
            } else {
                let span = create_span(&attrs);
                let opts = ContainerOptions::from_iter_named(ATTR_NAME, span, attrs)?;

                if opts.default {
                    validate_opts(&opts, &fields, span)?;
                }

                opts
            },
            fields,
            struct_name,
            generics,
        })
    }
}

fn validate_opts(opts: &ContainerOptions, fields: &FieldsSource, span: Span) -> Result<(), Error> {
    if opts.args.is_empty() && fields.fields().is_argless() {
        Ok(())
    } else {
        Err(Error::new(
            span,
            "The constructor cannot have any arguments if the `default` option is used",
        ))
    }
}

pub fn create_span<'a, S>(it: impl IntoIterator<Item = &'a S>) -> Span
where
    S: Spanned + 'a,
{
    it.into_iter()
        .map(Spanned::span)
        .reduce(move |a, b| if let Some(span) = a.join(b) { span } else { a })
        .unwrap_or_else(Span::call_site)
}

fn find_variant<P>(span: &impl Spanned, variants: Punctuated<Variant, P>) -> syn::Result<Variant> {
    let mut out = None;
    for variant in variants {
        if variant
            .attrs
            .iter()
            .any(move |a| a.path().is_ident(ATTR_NAME))
        {
            if out.is_some() {
                return Err(Error::new_spanned(
                    variant.ident,
                    "Multiple variants marked with `#[new]`",
                ));
            }
            out = Some(variant);
        }
    }

    if let Some(out) = out {
        Ok(out)
    } else {
        Err(Error::new(
            span.span(),
            "Expected a variant marked with `#[new]`",
        ))
    }
}