static_table_derive 0.1.17

Procedural macros for investments crate
Documentation
#![recursion_limit="128"]

extern crate proc_macro;

use darling::FromMeta;
use proc_macro::TokenStream;
use proc_macro2::{Ident, Span};
use quote::quote;
use syn::{self, DeriveInput, Fields, Data, DataStruct, Meta, MetaList, MetaNameValue};

type GenericError = Box<dyn std::error::Error + Send + Sync>;
type GenericResult<T> = Result<T, GenericError>;

#[cfg_attr(test, derive(Debug))]
struct Column {
    id: String,
    name: Option<String>,
    alignment: Option<String>,
}

const TABLE_ATTR_NAME: &str = "table";
const COLUMN_ATTR_NAME: &str = "column";

macro_rules! Err {
    ($($arg:tt)*) => (::std::result::Result::Err(format!($($arg)*).into()))
}

#[proc_macro_derive(StaticTable, attributes(table, column))]
pub fn static_table_derive(input: TokenStream) -> TokenStream {
    match static_table_derive_impl(input) {
        Ok(output) => output,
        Err(err) => panic!("{}", err),
    }
}

fn static_table_derive_impl(input: TokenStream) -> GenericResult<TokenStream> {
    let ast: DeriveInput = syn::parse(input)?;
    let span = Span::call_site();

    let table_name = get_table_params(&ast)?;
    let columns = get_table_columns(&ast)?;

    let mod_ident = quote!(crate::formatting::table);
    let table_ident = Ident::new(&table_name, span);
    let row_proxy_ident = Ident::new(&(table_name + "RowProxy"), span);
    let row_ident = &ast.ident;

    let field_idents = columns.iter().map(|column| {
        Ident::new(&column.id, span)
    });

    let columns_init_code = columns.iter().map(|column| {
        let name = column.name.as_ref().unwrap_or(&column.id);
        match column.alignment {
            Some(ref alignment) => {
                let alignment_ident = Ident::new(&alignment.to_uppercase(), span);
                quote!(#mod_ident::Column::new_aligned(
                    #name, #mod_ident::Alignment::#alignment_ident))
            },
            None => quote!(#mod_ident::Column::new(#name))
        }
    });

    let column_modify_code = columns.iter().enumerate().map(|(index, column)| {
        let rename_method_ident = Ident::new(&format!("rename_{}", column.id), span);
        let hide_method_ident = Ident::new(&format!("hide_{}", column.id), span);
        quote! {
            fn #rename_method_ident(&mut self, name: &'static str) {
                self.table.rename_column(#index, name);
            }

            fn #hide_method_ident(&mut self) {
                self.table.hide_column(#index);
            }
        }
    });

    let cell_set_code = columns.iter().enumerate().map(|(index, column)| {
        let method_ident = Ident::new(&format!("set_{}", column.id), span);
        quote! {
            fn #method_ident<C: ::std::convert::Into<#mod_ident::Cell>>(&mut self, cell: C) {
                self.row[#index] = cell.into();
            }
        }
    });

    Ok(quote! {
        struct #table_ident {
            table: #mod_ident::Table,
        }

        impl #table_ident {
            fn new() -> #table_ident {
                let columns = vec![#(#columns_init_code,)*];
                #table_ident {
                    table: #mod_ident::Table::new(columns),
                }
            }

            fn add_row(&mut self, row: #row_ident) -> #row_proxy_ident {
                let row = self.table.add_row(row.into());
                #row_proxy_ident {row: row}
            }

            fn add_empty_row(&mut self) -> #row_proxy_ident {
                let row = self.table.add_empty_row();
                #row_proxy_ident {row: row}
            }

            fn is_empty(&self) -> bool {
                self.table.is_empty()
            }

            #(#column_modify_code)*

            fn print(&self, title: &str) {
                self.table.print(title);
            }
        }

        struct #row_proxy_ident<'a> {
            row: &'a mut #mod_ident::Row,
        }

        impl<'a> #row_proxy_ident<'a> {
            #(#cell_set_code)*
        }

        impl<'a, 'b> ::core::iter::IntoIterator for &'a mut #row_proxy_ident<'b> {
            type Item = &'a mut #mod_ident::Cell;
            type IntoIter = ::std::slice::IterMut<'a, #mod_ident::Cell>;

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

        impl ::std::convert::Into<#mod_ident::Row> for #row_ident {
            fn into(self) -> #mod_ident::Row {
                vec![#(self.#field_idents.into(),)*]
            }
        }
    }.into())
}

fn get_table_params(ast: &DeriveInput) -> GenericResult<String> {
    #[derive(FromMeta)]
    struct TableParams {
        name: String,
    }

    let mut table_name = None;

    for attr in &ast.attrs {
        let meta = attr.parse_meta().map_err(|e| format!(
            "Failed to parse `{:#?}`: {}", attr, e))?;

        if match_attribute_name(&meta, COLUMN_ATTR_NAME) {
            return Err!("{:?} attribute is allowed on struct fields only", COLUMN_ATTR_NAME);
        } else if !match_attribute_name(&meta, TABLE_ATTR_NAME) {
            continue;
        }

        let params = TableParams::from_meta(&meta).map_err(|e| format!(
            "{:?} attribute validation error: {}", TABLE_ATTR_NAME, e))?;

        if table_name.replace(params.name).is_some() {
            return Err!("Duplicated {:?} attribute", TABLE_ATTR_NAME);
        }
    }

    Ok(table_name.unwrap_or_else(|| String::from("Table")))
}

fn get_table_columns(ast: &DeriveInput) -> GenericResult<Vec<Column>> {
    #[derive(FromMeta, Default)]
    struct ColumnParams {
        #[darling(default)]
        name: Option<String>,
        #[darling(default)]
        align: Option<String>,
    }

    let mut columns = Vec::new();

    let fields = match ast.data {
        Data::Struct(DataStruct{fields: Fields::Named(ref fields), ..}) => &fields.named,
        _ => return Err!("A struct with named fields is expected"),
    };

    for field in fields {
        let field_name = field.ident.as_ref()
            .ok_or("A struct with named fields is expected")?.to_string();
        let mut field_params = None;

        for attr in &field.attrs {
            let meta = attr.parse_meta().map_err(|e| format!(
                "Failed to parse `{:#?}` on {:?} field: {}", attr, field_name, e))?;

            if match_attribute_name(&meta, TABLE_ATTR_NAME) {
                return Err!("{:?} attribute is allowed on struct definition only", TABLE_ATTR_NAME);
            } else if !match_attribute_name(&meta, COLUMN_ATTR_NAME) {
                continue;
            }

            let params = ColumnParams::from_meta(&meta).map_err(|e| format!(
                "{:?} attribute on {:?} field validation error: {}", COLUMN_ATTR_NAME, field_name, e))?;

            match params.align.as_deref() {
                Some("left") | Some("center") | Some("right") | None => {},
                _ => return Err!("Invalid alignment of {:?}: {:?}",
                                 field_name, params.align.unwrap()),
            };

            if field_params.replace(params).is_some() {
                return Err!("Duplicated {:?} attribute on {:?} field", COLUMN_ATTR_NAME, field_name);
            }
        }

        let column_params = field_params.unwrap_or_default();
        columns.push(Column {
            id: field_name,
            name: column_params.name,
            alignment: column_params.align,
        })
    }

    Ok(columns)
}

fn match_attribute_name(meta: &Meta, name: &str) -> bool {
    let path = match meta {
        Meta::Path(path) => path,
        Meta::List(MetaList{path, ..}) => path,
        Meta::NameValue(MetaNameValue{path, ..}) => path,
    };

    path.segments.len() == 1 && path.segments.first().unwrap().ident == name
}