deltastruct_proc 0.1.8

The proc-macro backend for the crate Deltastruct.
Documentation
extern crate proc_macro;

use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{parse_macro_input, punctuated::Punctuated, token::Comma, Ident, Type};

use convert_case::{Case, Casing};

#[proc_macro_attribute]
pub fn deltastruct(attrs: TokenStream, input: TokenStream) -> TokenStream {
  let input = parse_macro_input!(input as syn::ItemMod);
  let args = parse_macro_input!(attrs as syn::AttributeArgs);
  parse_deltamod(input, parse_derives(args))
}

fn parse_derives(args: syn::AttributeArgs) -> Vec<syn::Path> {
  // see if we even got any
  if args.len() == 0 {
    return vec![];
  }

  // extract
  let mut res: Option<Vec<_>> = None;
  for att in args {
    res = match att {
      syn::NestedMeta::Meta(syn::Meta::List(p)) if p.path.is_ident("derive") => p
        .nested
        .iter()
        .map(|x| {
          if let syn::NestedMeta::Meta(syn::Meta::Path(y)) = x {
            Some(y.clone())
          } else {
            None
          }
        })
        .collect::<Option<Vec<syn::Path>>>(),
      _ => None,
    };
  }

  if let Some(res) = res {
    return res;
  } else {
    panic!("Invalid attributes detected")
  }
}

fn parse_deltamod(modblock: syn::ItemMod, derives: Vec<syn::Path>) -> TokenStream {
  let content = modblock
    .content
    .expect("Deltastruct requires content defined in a module definition")
    .1;

  // Generate names for new definitions
  let ident_struct = modblock.ident.clone();
  let ident_delta = format_ident!("{}Delta", modblock.ident);

  // Vec of fields contained in the defined struct
  let mut fields: Option<Punctuated<syn::Field, _>> = None;

  // Vec of methods that should be included as part of the delta
  let mut methods: Option<Vec<syn::ImplItemMethod>> = None;

  // Keep track of items to re-inject later
  let mut user_defined_items = Vec::<syn::Item>::new();

  // Pull out the relevant data
  for item in content {
    match item {
      syn::Item::Struct(s) => {
        // Make sure we hit the right type
        if s.ident != modblock.ident {
          continue;
        }

        user_defined_items.push(syn::Item::Struct(s.clone()));
        fields = match s.fields {
          syn::Fields::Named(nf) => Some(nf.named),
          _ => panic!("Expected named fields as part of defined struct"),
        }
      }
      syn::Item::Impl(i) => {
        // TODO: Check if the impl is for the correct struct. Currently
        // it's rather difficult because Item::Impls only have a type
        // field, not an identifier

        user_defined_items.push(syn::Item::Impl(i.clone()));
        methods = Some(
          i.items
            .iter()
            .filter_map(|x| match x {
              syn::ImplItem::Method(m) => Some((*m).clone()),
              _ => None,
            })
            .collect(),
        );
      }
      _ => continue,
    }
  }

  // Pre-Validatation
  let fields = fields.expect("No data field definition found.");
  let methods = methods.expect("No impl definition found.");

  // Process data into need identifiers and whatnot

  // extract field names
  let field_names = fields
    .iter()
    .enumerate()
    .map(|(i, field)| {
      field
        .ident
        .clone()
        .unwrap_or(format_ident!("f{}", i).clone())
    })
    .collect::<Vec<_>>();

  // convert field names to camel case
  let field_names_camel = field_names
    .iter()
    .map(|n| {
      format_ident!(
        "{}",
        n.to_string()
          .from_case(Case::Snake)
          .to_case(Case::UpperCamel)
      )
    })
    .collect::<Vec<_>>();

  // extract field types
  let field_types = fields.iter().map(|f| f.ty.clone()).collect::<Vec<_>>();

  // build identifier for each fn type
  let field_types_fn: Vec<_> = field_types
    .iter()
    .map(|ty| match ty {
      syn::Type::Path(p) => {
        let mut segs = p.path.segments.clone();
        let seg = segs.pop().expect("Empty type path on field.");
        let seg = seg.value();
        let nseg = syn::PathSegment {
          ident: format_ident!("{}Delta", seg.ident),
          ..seg.clone()
        };

        segs.push(nseg);
        segs
      }
      _ => panic!("Expected type path for field."),
    })
    .collect();

  // extract method names
  let fns_names: Vec<Ident> = methods.iter().map(|x| x.sig.ident.clone()).collect();

  // convert method names to camel case
  let fns_names_camel: Vec<Ident> = methods
    .iter()
    .map(|x| {
      format_ident!(
        "{}",
        x.sig
          .ident
          .to_string()
          .from_case(Case::Snake)
          .to_case(Case::UpperCamel)
      )
    })
    .collect();

  // extract method args
  let fns_args_types: Vec<Punctuated<Box<Type>, Comma>> = methods
    .iter()
    .map(|m| {
      m.sig
        .inputs
        .clone()
        .into_iter()
        .skip(1)
        .filter_map(|arg| match arg {
          syn::FnArg::Typed(targ) => Some(targ.ty),
          _ => None,
        })
        .collect()
    })
    .collect();

  // generate dummy arg names to match against for each method
  let fns_args_names: Vec<Punctuated<syn::Ident, Comma>> = fns_args_types
    .iter()
    .map(|ma| (0..ma.len()).map(|i| format_ident!("f{}", i)).collect())
    .collect::<Vec<_>>();

  // build fn_enum::variant pairs for matching
  let fns_self_match_names = fns_names_camel.iter().map(|x| {
    let mut res = Punctuated::<syn::PathSegment, syn::token::Colon2>::new();

    res.push(syn::PathSegment {
      ident: ident_delta.clone(),
      arguments: syn::PathArguments::None,
    });
    res.push(syn::PathSegment {
      ident: x.clone(),
      arguments: syn::PathArguments::None,
    });
    res
  });
  let fns_field_match_names = field_names_camel.iter().map(|x| {
    let mut res = Punctuated::<syn::PathSegment, syn::token::Colon2>::new();

    res.push(syn::PathSegment {
      ident: ident_delta.clone(),
      arguments: syn::PathArguments::None,
    });
    res.push(syn::PathSegment {
      ident: x.clone(),
      arguments: syn::PathArguments::None,
    });
    res
  });

  // Build the delta struct
  let token_delta_struct = quote!(
    #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
    #[derive(#(#derives),*)]
    pub enum #ident_delta {
      #(#fns_names_camel(#fns_args_types)),*,
      #(#field_names_camel(#field_types_fn)),*
    }
  );

  // Build impl
  let token_impl_match = quote!(
    #(#fns_self_match_names(#fns_args_names) => self.#fns_names(#fns_args_names)),*,
    #(#fns_field_match_names(ref x) => self.#field_names.apply(x)),*
  );
  let token_impl = quote!(
    impl DeltaStruct<#ident_delta> for #ident_struct {
      fn apply(&mut self, delta : &#ident_delta) {
        match *delta {
          #token_impl_match
        };
      }
    }
  );

  // Build the final set of definitions
  quote!(
    #(#user_defined_items)*
    #token_delta_struct
    #token_impl
  )
  .into()
}