fray-macro 0.1.2

Macros to generate bitfield structs for fray
Documentation
use darling::FromMeta;
use proc_macro2::Span;
use quote::{ToTokens, TokenStreamExt};
use syn::{
    Attribute, Ident, LitInt, Meta, MetaList, Path, parse::Parse, parse2, punctuated::Punctuated,
    spanned::Spanned, token::Comma,
};

#[derive(Debug, Clone, Copy)]
pub struct FieldAttrs {
    pub bits: Option<usize>,
}

impl TryFrom<&[Attribute]> for FieldAttrs {
    type Error = darling::Error;

    fn try_from(value: &[Attribute]) -> Result<Self, Self::Error> {
        let mut bits = None;
        for Attribute {
            pound_token: _,
            style: _,
            bracket_token: _,
            meta,
        } in value
        {
            match meta {
                Meta::Path(_path) => {}
                Meta::List(MetaList {
                    path,
                    delimiter: _,
                    tokens,
                }) => {
                    let ident = match path.get_ident() {
                        Some(ident) => Ok(ident),
                        None => Err(syn::Error::new(
                            path.span(),
                            format!("Expected ident got {path:?}"),
                        )),
                    }?;
                    let ident_str = ident.to_string();
                    match ident_str.as_str() {
                        "bits" => {
                            let size: LitInt = parse2(tokens.clone())?;
                            bits = Some(size.base10_parse()?);
                        }
                        ident_str => {
                            return Err(syn::Error::new(
                                path.span(),
                                format!("Unknown attribute {ident_str}"),
                            )
                            .into());
                        }
                    }
                }
                Meta::NameValue(_meta_name_value) => {}
            }
        }
        Ok(Self { bits })
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, FromMeta)]
pub enum BitOrder {
    #[default]
    Lsb0,
    Msb0,
}

#[allow(non_camel_case_types)]
#[derive(Debug, Clone, Copy, FromMeta, PartialEq, Eq)]
pub enum Repr {
    u8,
    u16,
    u32,
    u64,
}

impl ToTokens for Repr {
    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
        let str = match self {
            Repr::u8 => "u8",
            Repr::u16 => "u16",
            Repr::u32 => "u32",
            Repr::u64 => "u64",
        };
        tokens.append(Ident::new(str, Span::call_site()));
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Impls {
    Debug,
    // IntoInner,
}

impl Parse for Impls {
    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
        let ident: Ident = input.parse()?;
        let ident_str = ident.to_string().to_lowercase();
        Ok(match ident_str.as_str() {
            "debug" => Self::Debug,
            // "into_inner" => Self::IntoInner,
            ident_str => {
                return Err(syn::Error::new(
                    ident.span(),
                    format!("Unkown impl `{ident_str}`"),
                ));
            }
        })
    }
}

#[derive(Debug, FromMeta)]
#[darling(derive_syn_parse)]
pub struct BitFieldArgs {
    #[darling(default)]
    pub repr: Option<Repr>,
    #[darling(default)]
    pub container: List<Path>,
    #[darling(default)]
    pub derives: List<Path>,
    #[darling(default)]
    pub impls: List<Impls>,
    #[darling(default)]
    pub bitorder: BitOrder,
}

#[derive(Debug, Clone)]
pub struct List<T>(Punctuated<T, Comma>);

impl<T> List<T> {
    #[inline]
    pub fn len(&self) -> usize {
        self.0.len()
    }

    #[inline]
    pub fn first(&self) -> Option<&T> {
        self.0.first()
    }
}

impl<T: Parse> FromMeta for List<T> {
    fn from_meta(item: &Meta) -> darling::Result<Self> {
        match item {
            Meta::List(meta_list) => {
                let tokens = meta_list.clone().tokens;
                let list = parse2(tokens).map_err(darling::Error::from)?;
                Ok(list)
            }
            _ => Err(darling::Error::custom("Expected a list of identifiers")),
        }
    }
}

impl<T: Parse> Parse for List<T> {
    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
        Ok(Self(Punctuated::parse_terminated(input)?))
    }
}

impl<T> ToTokens for List<T>
where
    T: ToTokens,
{
    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
        self.0.to_tokens(tokens);
    }
}

impl<T> Default for List<T> {
    fn default() -> Self {
        Self(Default::default())
    }
}

impl<T> IntoIterator for List<T> {
    type Item = T;

    type IntoIter = <Punctuated<T, Comma> as IntoIterator>::IntoIter;

    fn into_iter(self) -> Self::IntoIter {
        self.0.into_iter()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use darling::ast::NestedMeta;
    use quote::quote;

    #[test]
    fn parse_repr() {
        let tokens = quote! {
            repr(u8),
        };
        let attrs = NestedMeta::parse_meta_list(tokens).unwrap();
        let bitfield_args = BitFieldArgs::from_list(&attrs).unwrap();
        assert_eq!(bitfield_args.repr, Some(Repr::u8));
    }

    #[test]
    fn parse_repr_invalid() {
        let tokens = quote! {
            repr(i8),
        };
        let attrs = NestedMeta::parse_meta_list(tokens).unwrap();
        let bitfield_args = BitFieldArgs::from_list(&attrs);
        assert!(bitfield_args.is_err());
    }

    #[test]
    fn parse_repr_multiple() {
        let tokens = quote! {
            repr(u8, u16),
        };
        let attrs = NestedMeta::parse_meta_list(tokens).unwrap();
        let bitfield_args = BitFieldArgs::from_list(&attrs);
        assert!(bitfield_args.is_err());
    }

    #[test]
    fn parse_container() {
        let tokens = quote! {
            container(u8),
        };
        let attrs = NestedMeta::parse_meta_list(tokens).unwrap();
        let bitfield_args = BitFieldArgs::from_list(&attrs).unwrap();
        let first = bitfield_args.container.first().unwrap();

        let ident_str = first.get_ident().unwrap().to_string();

        assert_eq!(ident_str, "u8");
    }

    #[test]
    fn parse_impls() {
        let tokens = quote! {
            impls(debug),
        };
        let attrs = NestedMeta::parse_meta_list(tokens).unwrap();
        let bitfield_args = BitFieldArgs::from_list(&attrs).unwrap();

        assert_eq!(bitfield_args.impls.len(), 1);
        assert_eq!(bitfield_args.impls.first(), Some(&Impls::Debug));
    }

    #[test]
    fn parse_bitorder() {
        let tokens = quote! {
            bitorder(msb0),
        };
        let attrs = NestedMeta::parse_meta_list(tokens).unwrap();
        let bitfield_args = BitFieldArgs::from_list(&attrs).unwrap();

        assert_eq!(bitfield_args.bitorder, BitOrder::Msb0);
    }

    #[test]
    fn parse_bitorder_default() {
        let tokens = quote! {};
        let attrs = NestedMeta::parse_meta_list(tokens).unwrap();
        let bitfield_args = BitFieldArgs::from_list(&attrs).unwrap();

        assert_eq!(bitfield_args.bitorder, BitOrder::Lsb0);
    }
}