preftool-derive-core 0.2.0

Configuration library for CLI tools/servers.
Documentation
#![recursion_limit = "4096"]

extern crate darling;
extern crate proc_macro2;
extern crate syn;
#[macro_use]
extern crate quote;

use darling::util::Override;
use darling::{FromDeriveInput, FromField, FromMeta, FromVariant};
use proc_macro2::TokenStream;
use quote::ToTokens;
use syn::spanned::Spanned;

#[derive(Debug)]
enum Validation {
  None,
  DefaultTrait,
  Custom(syn::Path),
}

impl Default for Validation {
  fn default() -> Self {
    Validation::None
  }
}

impl ToTokens for Validation {
  fn to_tokens(&self, tokens: &mut TokenStream) {
    let validation_impl = match self {
      Validation::None => quote! { ::std::result::Result::Ok(()) },
      Validation::DefaultTrait => quote! { <::preftool::ValidateOptions>::validate(self) },
      Validation::Custom(p) => quote_spanned! { p.span()=> #p(self) },
    };

    let fn_impl = quote! {
      fn validate(&self) -> ::preftool::ValidationResult<()> {
        #validation_impl
      }
    };

    fn_impl.to_tokens(tokens)
  }
}

impl FromMeta for Validation {
  fn from_meta(item: &syn::Meta) -> darling::Result<Self> {
    let val = <Option<Override<syn::Path>> as FromMeta>::from_meta(item)?;
    Ok(match val {
      None => Validation::None,
      Some(Override::Inherit) => Validation::DefaultTrait,
      Some(Override::Explicit(path)) => Validation::Custom(path),
    })
  }
}

#[derive(Debug, FromField)]
#[darling(attributes(preftool), forward_attrs)]
struct FieldOptions {
  ident: Option<syn::Ident>,
  ty: syn::Type,

  #[darling(default)]
  flatten: darling::util::Flag,

  #[darling(default)]
  name: Option<String>,

  #[darling(default)]
  from: Option<syn::Path>,

  #[darling(forward_attrs)]
  attrs: Vec<syn::Attribute>,
}

impl ToTokens for FieldOptions {
  fn to_tokens(&self, tokens: &mut TokenStream) {
    let ident = self.ident.as_ref().unwrap();
    let ty = &self.ty;
    let section = if self.flatten.is_some() {
      quote! { config }
    } else {
      let name = self.name.clone().unwrap_or_else(|| ident.to_string());
      quote! { &config.section(#name) }
    };

    let bind_result = match &self.from {
      None => quote! { ::preftool::Options::bind(&mut self.#ident, #section) },
      Some(p) => {
        quote! { <#p as ::preftool::ConfigProxy<#ty>>::bind_proxy(&mut self.#ident, #section) }
      }
    };

    let out = quote! {
      if let ::std::result::Result::Err(error) = #bind_result {
        ::preftool::ValidationError::append_to(error, &mut __errors);
      }
    };

    out.to_tokens(tokens);
  }
}

#[derive(Debug, FromVariant)]
#[darling(attributes(preftool))]
struct VariantOptions {
  ident: syn::Ident,
  fields: darling::ast::Fields<FieldOptions>,
}

#[derive(Debug, FromDeriveInput)]
#[darling(attributes(preftool))]
struct DeriveOptions {
  ident: syn::Ident,
  generics: syn::Generics,
  #[darling(default, rename = "validate")]
  validation: Validation,
  data: darling::ast::Data<VariantOptions, FieldOptions>,
}

impl ToTokens for DeriveOptions {
  fn to_tokens(&self, tokens: &mut TokenStream) {
    let (impl_generics, ty_generics, where_clause) = self.generics.split_for_impl();
    let DeriveOptions {
      ident,
      validation,
      data,
      ..
    } = self;

    let field_impls = match data {
      darling::ast::Data::Enum(_variants) => unimplemented!(),
      darling::ast::Data::Struct(fields) => {
        let fields = &fields.fields;
        quote! { #(#fields)* }
      }
    };

    let out = quote! {
      impl #impl_generics ::preftool::Options for #ident #ty_generics #where_clause {
        fn bind_config<C: ::preftool::Configuration>(&mut self, config: &C) -> ::preftool::ValidationResult<()> {
          let mut __errors: ::std::vec::Vec<::preftool::ValidationError> = ::std::vec::Vec::new();
          #(#field_impls)*
          if !__errors.is_empty() {
            return Err(__errors.into())
          }

          Ok(())
        }

        #validation
      }
    };

    out.to_tokens(tokens)
  }
}

pub fn derive_options(input: &syn::DeriveInput) -> TokenStream {
  match DeriveOptions::from_derive_input(input) {
    Ok(val) => val.into_token_stream(),
    Err(err) => err.write_errors(),
  }
}