cmdparse-derive 0.1.1

Derive macro implementation for cmdparse.
Documentation
use crate::attributes::{BuildableAttributes, VariantAttributes};
use crate::context::CodegenContext;
use crate::fields::FieldsSet;

#[derive(Debug, PartialEq, Eq)]
struct Variant {
    label: String,
    fieldset_index: usize,
}

struct VariantFieldsSet<'a> {
    ident: &'a syn::Ident,
    fieldset: FieldsSet<'a>,
}

pub(crate) struct VariantsSet<'a> {
    variants: Vec<Variant>,
    fieldsets: Vec<VariantFieldsSet<'a>>,
    transparent: Vec<(usize, bool)>,
}

pub(crate) struct VariantView<'a> {
    pub(crate) ident: &'a syn::Ident,
    pub(crate) fields: &'a FieldsSet<'a>,
    pub(crate) label: &'a str,
}

pub(crate) struct TransparentVariantView<'a> {
    pub(crate) ident: &'a syn::Ident,
    pub(crate) fields: &'a FieldsSet<'a>,
    pub(crate) ignore_error: bool,
}

impl<'a> VariantsSet<'a> {
    pub(crate) fn from_variants(
        context: &mut CodegenContext<'a>,
        variants: impl Iterator<Item = &'a syn::Variant>,
    ) -> Result<Self, syn::Error> {
        let mut result = VariantsSet {
            variants: Vec::new(),
            fieldsets: Vec::new(),
            transparent: Vec::new(),
        };

        for variant in variants {
            let attributes = VariantAttributes::from_attributes(variant.attrs.iter())?;
            if attributes.ignored && attributes.aliases.is_empty() {
                continue;
            }

            let fieldset = FieldsSet::from_fields(context, &variant.fields)?;
            let fieldset_index = result.fieldsets.len();
            result.fieldsets.push(VariantFieldsSet {
                ident: &variant.ident,
                fieldset,
            });

            if let Some(no_error) = attributes.transparent_no_error {
                result.transparent.push((fieldset_index, no_error));
            } else {
                if !attributes.ignored {
                    let label = attributes
                        .renamed
                        .unwrap_or_else(|| variant_to_kebab_case(&variant.ident.to_string()));
                    result.variants.push(Variant {
                        label,
                        fieldset_index,
                    });
                }
                for alias in attributes.aliases {
                    result.variants.push(Variant {
                        label: alias,
                        fieldset_index,
                    });
                }
            }
        }

        Ok(result)
    }

    pub(crate) fn variant_views(&self) -> impl Iterator<Item = VariantView<'_>> {
        self.variants.iter().map(|variant| {
            let fields = &self.fieldsets[variant.fieldset_index];
            VariantView {
                ident: fields.ident,
                fields: &fields.fieldset,
                label: variant.label.as_str(),
            }
        })
    }

    pub(crate) fn transparent_variants(&self) -> impl Iterator<Item = TransparentVariantView> {
        self.transparent.iter().map(|(index, no_error)| {
            let fields = &self.fieldsets[*index];
            TransparentVariantView {
                ident: fields.ident,
                fields: &fields.fieldset,
                ignore_error: *no_error,
            }
        })
    }
}

fn variant_to_kebab_case(ident: &str) -> String {
    let mut result = String::new();
    for (i, ch) in ident.chars().enumerate() {
        let lowercase = ch.to_ascii_lowercase();
        if i > 0 && ch != lowercase {
            result.push('-');
        }
        result.push(lowercase);
    }
    result
}

#[cfg(test)]
mod tests {
    use super::{variant_to_kebab_case, Variant, VariantsSet};
    use crate::context::MockCodegenContext;
    use quote::quote;

    #[test]
    fn rename_variant() {
        assert_eq!(&variant_to_kebab_case("Word"), "word");
        assert_eq!(&variant_to_kebab_case("TwoWords"), "two-words");
    }

    #[test]
    fn simple_variants() {
        let enum_ = quote! { enum Mock {
            First,
            Second(u8),
            MultipleWords,
            #[cmd(rename = "new-name")] Renamed,
            #[cmd(alias = "alias-1", alias = "alias-2")] WithAliases,
            #[cmd(ignore, alias = "ignored-1", alias = "ignored-2")] AliasesOnly,
            #[cmd(ignore)] Ignored,
        }};
        let variants = syn::parse2::<syn::ItemEnum>(enum_).unwrap().variants;
        let mock_context = MockCodegenContext::default();
        let mut context = mock_context.context();
        let variantsset = VariantsSet::from_variants(&mut context, variants.iter()).unwrap();

        let expected_variants = [
            ("first", 0),
            ("second", 1),
            ("multiple-words", 2),
            ("new-name", 3),
            ("with-aliases", 4),
            ("alias-1", 4),
            ("alias-2", 4),
            ("ignored-1", 5),
            ("ignored-2", 5),
        ];
        assert_eq!(
            variantsset.variants,
            expected_variants
                .iter()
                .map(|(label, index)| Variant {
                    label: label.to_string(),
                    fieldset_index: *index
                })
                .collect::<Vec<_>>(),
        );

        let variant_fields: Vec<_> = variantsset
            .fieldsets
            .iter()
            .map(|fs| fs.ident.to_string())
            .collect();
        assert_eq!(
            variant_fields,
            vec![
                "First".to_string(),
                "Second".to_string(),
                "MultipleWords".to_string(),
                "Renamed".to_string(),
                "WithAliases".to_string(),
                "AliasesOnly".to_string(),
            ]
        );
    }
}