tf-bindgen-codegen 0.1.0

A collection of proc-macros to improve tf-bindgen code.
Documentation
use heck::ToUpperCamelCase;
use quote::ToTokens;
use quote::__private::TokenStream;
use syn::parse::{Parse, ParseStream};
use syn::Token;

mod keyword {
    syn::custom_keyword!(resource);
    syn::custom_keyword!(scope);
}

pub struct Block {
    scope: syn::Expr,
    ty: syn::LitStr,
    name: syn::LitStr,
    body: Body,
}

pub struct Body {
    attributes: Vec<Attribute>,
}

pub enum Attribute {
    Block { name: syn::Ident, body: Body },
    Field { name: syn::Ident, assign: syn::Expr },
}

impl ToTokens for Block {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        let name = self.ty.value().to_upper_camel_case();
        let name = syn::Ident::new(&name, self.ty.span());
        let module = self.ty.value();
        let module = syn::Ident::new(&module, self.ty.span());
        let setter = self.body.to_setter_tokens();
        let blocks = self.body.to_block_tokens(&name, &module);
        let scope = &self.scope;
        let name_str = &self.name;
        tokens.extend(quote::quote!(
            {
                #( #blocks )*
                #module::#name::create(#scope, #name_str)
                    #( #setter )*
                    .build()
            }
        ))
    }
}

impl Body {
    fn to_setter_tokens(&self) -> impl Iterator<Item = TokenStream> + '_ {
        use Attribute::*;
        self.attributes
            .iter()
            .enumerate()
            .map(|(i, attr)| match attr {
                Block { name, .. } => {
                    let field_name = format!("{name}{i}");
                    let field_name = syn::Ident::new(&field_name, name.span());
                    quote::quote!(.#name(#field_name))
                }
                Field { name, assign } => quote::quote!(.#name(#assign)),
            })
    }

    fn to_block_tokens<'a>(
        &'a self,
        name: &'a syn::Ident,
        module: &'a syn::Ident,
    ) -> impl Iterator<Item = TokenStream> + 'a {
        use Attribute::*;
        self.attributes
            .iter()
            .enumerate()
            .filter_map(move |(i, attr)| match attr {
                Block { name: aname, body } => {
                    let n = format!("{name}{}", aname.to_string().to_upper_camel_case());
                    let n = syn::Ident::new(&n, aname.span());
                    let aname = syn::Ident::new(&format!("{aname}{i}"), aname.span());
                    Some((aname, body.to_tokens(n, module)))
                }
                _ => None,
            })
            .map(|(name, body)| quote::quote!( let #name = #body; ))
    }

    fn to_tokens(&self, name: syn::Ident, module: &syn::Ident) -> TokenStream {
        let setter = self.to_setter_tokens();
        let blocks = self.to_block_tokens(&name, module);
        quote::quote!(
            {
                #( #blocks )*
                #module::#name::builder()
                    #( #setter )*
                    .build()
            }
        )
    }
}

impl Parse for Block {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let scope = input.parse()?;
        let _: Token![,] = input.parse()?;
        let _: keyword::resource = input.parse()?;
        let ty = input.parse()?;
        let name = input.parse()?;
        let body = input.parse()?;
        Ok(Self {
            scope,
            ty,
            name,
            body,
        })
    }
}

impl Parse for Body {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let content;
        let _ = syn::braced!(content in input);
        let attributes = (0..)
            .take_while(|_| !content.is_empty())
            .map(|_| content.parse())
            .collect::<Result<_, _>>()?;
        Ok(Self { attributes })
    }
}

impl Parse for Attribute {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let name = input.parse()?;
        if input.peek(Token![=]) {
            let _: Token![=] = input.parse()?;
            let assign = input.parse()?;
            return Ok(Attribute::Field { name, assign });
        }
        let body = input.parse()?;
        Ok(Attribute::Block { name, body })
    }
}