enum_kind 0.2.0

Easily manage values related to enum.
Documentation
use crate::{input::*, util::is_bool};
use pmutil::{Quote, SpanExt};
use swc_macros_common::prelude::*;
use syn::*;

pub fn expand(
    Input {
        attrs,
        name,
        variants,
        generics,
        vis,
    }: Input,
) -> Item {
    // verify variant attributes.
    {
        for v in &variants {
            if v.attrs.has_delegate {
                match v.data {
                    Fields::Named(FieldsNamed {
                        named: ref fields, ..
                    })
                    | Fields::Unnamed(FieldsUnnamed {
                        unnamed: ref fields,
                        ..
                    }) if fields.len() == 1 => {}
                    _ => panic!(
                        "currently #[kind(delegate)] can be applied to variant with only one field"
                    ),
                }
            }
            for value in &v.attrs.fn_values {
                let used = attrs
                    .fns
                    .iter()
                    .map(|f| &f.name)
                    .any(|fn_name| value.fn_name == *fn_name || value.fn_name == "delegate");
                if !used {
                    panic!("Unknown function `{}` on variant {}", value.fn_name, v.name)
                }
            }
        }
    }

    let items = attrs
        .fns
        .into_iter()
        .map(|f| f.expand(&name, vis.clone(), &variants))
        .map(ImplItem::Method)
        .fold(TokenStream::new(), |mut t, i| {
            i.to_tokens(&mut t);
            t
        });

    Quote::new_call_site()
        .quote_with(smart_quote!(
            Vars {
                Type: name,
                items,
            },
            {
                impl Type {
                    items
                }
            }
        ))
        .parse::<ItemImpl>()
        .with_generics(generics)
        .into()
}

impl FnDef {
    fn expand(self, enum_name: &Ident, vis: Visibility, variants: &[EnumVar]) -> ImplItemMethod {
        let FnDef {
            name,
            return_type,
            default_value,
        } = self;

        let name_span = name.span();

        let arms =
            variants
                .iter()
                .map(|v| -> Arm {
                    // Bind this variant.
                    let (pat, mut fields) =
                        VariantBinder::new(Some(enum_name), &v.name, &v.data, &v.attrs.extras)
                            .bind("_", Some(call_site()), None);

                    let body = {
                        let value = match v
                            .attrs
                            .fn_values
                            .iter()
                            .find(|fn_val| fn_val.fn_name == name)
                            .map(|attr| attr.value.clone())
                        {
                            Some(Some(value)) => Some(value),

                            // not specified, but has `#[kind(delegate)]`
                            None if v.attrs.has_delegate => {
                                assert_eq!(fields.len(), 1);
                                let field = fields.remove(0);
                                Some(
                                    Quote::new_call_site()
                                        .quote_with(smart_quote!(
                                            Vars {
                                                field,
                                                method: &name,
                                            },
                                            { field.method() }
                                        ))
                                        .parse(),
                                )
                            }

                            // if return type is bool and attribute is specified, value is true.
                            Some(None) if is_bool(&return_type) => Some(Expr::Lit(ExprLit {
                                attrs: Default::default(),
                                lit: Lit::Bool(LitBool {
                                    value: true,
                                    span: Span::call_site(),
                                }),
                            })),
                            _ => None,
                        };

                        value
                            .or_else(|| default_value.clone())
                            .map(Box::new)
                            .unwrap_or_else(|| {
                                panic!(
                                    "value of {fn_name} for {variant} is not specified.",
                                    fn_name = name,
                                    variant = v.name
                                );
                            })
                    };

                    Arm {
                        pat,
                        body,

                        // Forward cfg attributes.
                        attrs: v
                            .attrs
                            .extras
                            .iter()
                            .filter(|attr| is_attr_name(attr, "cfg"))
                            .cloned()
                            .collect(),
                        fat_arrow_token: call_site(),
                        comma: Some(call_site()),
                        guard: None,
                    }
                })
                .collect();

        // match self {}
        let match_expr = Expr::Match(ExprMatch {
            attrs: Default::default(),
            match_token: call_site(),
            brace_token: call_site(),

            expr: Quote::new_call_site()
                .quote_with(smart_quote!(Vars {}, { self }))
                .parse::<Expr>()
                .into(),

            arms,
        });

        ImplItemMethod {
            // fn (&self) -> ReturnTpe
            sig: Signature {
                asyncness: None,
                constness: None,
                unsafety: None,
                abi: None,
                fn_token: name.span().as_token(),
                paren_token: name.span().as_token(),
                inputs: vec![
                    // TODO
                    Element::End(FnArg::Receiver(Receiver {
                        reference: Some((name_span.as_token(), None)),
                        self_token: name_span.as_token(),
                        mutability: None,
                        attrs: Default::default(),
                    })),
                ]
                .into_iter()
                .collect(),
                ident: name,
                generics: Default::default(),
                variadic: None,
                output: ReturnType::Type(name_span.as_token(), Box::new(return_type)),
            },

            block: Block {
                brace_token: call_site(),
                stmts: vec![Stmt::Expr(match_expr)],
            },

            // TODO
            vis,

            attrs: Default::default(),
            defaultness: None,
        }
    }
}