Skip to content

Commit

Permalink
Merge pull request #3484 from topecongiro/config_derive
Browse files Browse the repository at this point in the history
Add config_proc_macro
  • Loading branch information
topecongiro committed Apr 29, 2019
2 parents aa53d2d + 3dec18a commit d3ec460
Show file tree
Hide file tree
Showing 14 changed files with 548 additions and 187 deletions.
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ dirs = "1.0.4"
ignore = "0.4.6"
annotate-snippets = { version = "0.5.0", features = ["ansi_term"] }

config_proc_macro = { path = "config_proc_macro" }

# A noop dependency that changes in the Rust repository, it's a bit of a hack.
# See the `src/tools/rustc-workspace-hack/README.md` file in `rust-lang/rust`
# for more information.
Expand Down
1 change: 1 addition & 0 deletions config_proc_macro/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
target/
68 changes: 68 additions & 0 deletions config_proc_macro/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions config_proc_macro/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[package]
name = "config_proc_macro"
version = "0.1.0"
authors = ["topecongiro <seuchida@gmail.com>"]
edition = "2018"

[lib]
proc-macro = true

[dependencies]
proc-macro2 = "0.4"
quote = "0.6"
syn = { version = "0.15", features = ["full", "visit"] }

[dev-dependencies]
serde = { version = "1.0", features = ["derive"] }

[features]
default = []
debug-with-rustfmt = []
57 changes: 57 additions & 0 deletions config_proc_macro/src/attrs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//! This module provides utilities for handling attributes on variants
//! of `config_type` enum. Currently there are two types of attributes
//! that could appear on the variants of `config_type` enum: `doc_hint`
//! and `value`. Both comes in the form of name-value pair whose value
//! is string literal.

/// Returns the value of the first `doc_hint` attribute in the given slice or
/// `None` if `doc_hint` attribute is not available.
pub fn find_doc_hint(attrs: &[syn::Attribute]) -> Option<String> {
attrs.iter().filter_map(doc_hint).next()
}

/// Returns `true` if the given attribute is a `doc_hint` attribute.
pub fn is_doc_hint(attr: &syn::Attribute) -> bool {
is_attr_name_value(attr, "doc_hint")
}

/// Returns a string literal value if the given attribute is `doc_hint`
/// attribute or `None` otherwise.
pub fn doc_hint(attr: &syn::Attribute) -> Option<String> {
get_name_value_str_lit(attr, "doc_hint")
}

/// Returns the value of the first `value` attribute in the given slice or
/// `None` if `value` attribute is not available.
pub fn find_config_value(attrs: &[syn::Attribute]) -> Option<String> {
attrs.iter().filter_map(config_value).next()
}

/// Returns a string literal value if the given attribute is `value`
/// attribute or `None` otherwise.
pub fn config_value(attr: &syn::Attribute) -> Option<String> {
get_name_value_str_lit(attr, "value")
}

/// Returns `true` if the given attribute is a `value` attribute.
pub fn is_config_value(attr: &syn::Attribute) -> bool {
is_attr_name_value(attr, "value")
}

fn is_attr_name_value(attr: &syn::Attribute, name: &str) -> bool {
attr.interpret_meta().map_or(false, |meta| match meta {
syn::Meta::NameValue(syn::MetaNameValue { ref ident, .. }) if ident == name => true,
_ => false,
})
}

fn get_name_value_str_lit(attr: &syn::Attribute, name: &str) -> Option<String> {
attr.interpret_meta().and_then(|meta| match meta {
syn::Meta::NameValue(syn::MetaNameValue {
ref ident,
lit: syn::Lit::Str(ref lit_str),
..
}) if ident == name => Some(lit_str.value()),
_ => None,
})
}
15 changes: 15 additions & 0 deletions config_proc_macro/src/config_type.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
use proc_macro2::TokenStream;

use crate::item_enum::define_config_type_on_enum;
use crate::item_struct::define_config_type_on_struct;

/// Defines `config_type` on enum or struct.
// FIXME: Implement this on struct.
pub fn define_config_type(input: &syn::Item) -> TokenStream {
match input {
syn::Item::Struct(st) => define_config_type_on_struct(st),
syn::Item::Enum(en) => define_config_type_on_enum(en),
_ => panic!("Expected enum or struct"),
}
.unwrap()
}
178 changes: 178 additions & 0 deletions config_proc_macro/src/item_enum.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
use proc_macro2::TokenStream;
use quote::quote;

use crate::attrs::*;
use crate::utils::*;

type Variants = syn::punctuated::Punctuated<syn::Variant, syn::Token![,]>;

/// Defines and implements `config_type` enum.
pub fn define_config_type_on_enum(em: &syn::ItemEnum) -> syn::Result<TokenStream> {
let syn::ItemEnum {
vis,
enum_token,
ident,
generics,
variants,
..
} = em;

let mod_name_str = format!("__define_config_type_on_enum_{}", ident);
let mod_name = syn::Ident::new(&mod_name_str, ident.span());
let variants = fold_quote(variants.iter().map(process_variant), |meta| quote!(#meta,));

let impl_doc_hint = impl_doc_hint(&em.ident, &em.variants);
let impl_from_str = impl_from_str(&em.ident, &em.variants);
let impl_serde = impl_serde(&em.ident, &em.variants);
let impl_deserialize = impl_deserialize(&em.ident, &em.variants);

Ok(quote! {
#[allow(non_snake_case)]
mod #mod_name {
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub #enum_token #ident #generics { #variants }
#impl_doc_hint
#impl_from_str
#impl_serde
#impl_deserialize
}
#vis use #mod_name::#ident;
})
}

/// Remove attributes specific to `config_proc_macro` from enum variant fields.
fn process_variant(variant: &syn::Variant) -> TokenStream {
let metas = variant
.attrs
.iter()
.filter(|attr| !is_doc_hint(attr) && !is_config_value(attr));
let attrs = fold_quote(metas, |meta| quote!(#meta));
let syn::Variant { ident, fields, .. } = variant;
quote!(#attrs #ident #fields)
}

fn impl_doc_hint(ident: &syn::Ident, variants: &Variants) -> TokenStream {
let doc_hint = variants
.iter()
.map(doc_hint_of_variant)
.collect::<Vec<_>>()
.join("|");
let doc_hint = format!("[{}]", doc_hint);
quote! {
use crate::config::ConfigType;
impl ConfigType for #ident {
fn doc_hint() -> String {
#doc_hint.to_owned()
}
}
}
}

fn impl_from_str(ident: &syn::Ident, variants: &Variants) -> TokenStream {
let vs = variants
.iter()
.filter(|v| is_unit(v))
.map(|v| (config_value_of_variant(v), &v.ident));
let if_patterns = fold_quote(vs, |(s, v)| {
quote! {
if #s.eq_ignore_ascii_case(s) {
return Ok(#ident::#v);
}
}
});
quote! {
impl ::std::str::FromStr for #ident {
type Err = &'static str;

fn from_str(s: &str) -> Result<Self, Self::Err> {
#if_patterns
return Err("Bad variant");
}
}
}
}

fn doc_hint_of_variant(variant: &syn::Variant) -> String {
find_doc_hint(&variant.attrs).unwrap_or(variant.ident.to_string())
}

fn config_value_of_variant(variant: &syn::Variant) -> String {
find_config_value(&variant.attrs).unwrap_or(variant.ident.to_string())
}

fn impl_serde(ident: &syn::Ident, variants: &Variants) -> TokenStream {
let arms = fold_quote(variants.iter(), |v| {
let v_ident = &v.ident;
let pattern = match v.fields {
syn::Fields::Named(..) => quote!(#ident::v_ident{..}),
syn::Fields::Unnamed(..) => quote!(#ident::#v_ident(..)),
syn::Fields::Unit => quote!(#ident::#v_ident),
};
let option_value = config_value_of_variant(v);
quote! {
#pattern => serializer.serialize_str(&#option_value),
}
});

quote! {
impl ::serde::ser::Serialize for #ident {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: ::serde::ser::Serializer,
{
use serde::ser::Error;
match self {
#arms
_ => Err(S::Error::custom(format!("Cannot serialize {:?}", self))),
}
}
}
}
}

// Currently only unit variants are supported.
fn impl_deserialize(ident: &syn::Ident, variants: &Variants) -> TokenStream {
let supported_vs = variants.iter().filter(|v| is_unit(v));
let if_patterns = fold_quote(supported_vs, |v| {
let config_value = config_value_of_variant(v);
let variant_ident = &v.ident;
quote! {
if #config_value.eq_ignore_ascii_case(s) {
return Ok(#ident::#variant_ident);
}
}
});

let supported_vs = variants.iter().filter(|v| is_unit(v));
let allowed = fold_quote(supported_vs.map(config_value_of_variant), |s| quote!(#s,));

quote! {
impl<'de> serde::de::Deserialize<'de> for #ident {
fn deserialize<D>(d: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::{Error, Visitor};
use std::marker::PhantomData;
use std::fmt;
struct StringOnly<T>(PhantomData<T>);
impl<'de, T> Visitor<'de> for StringOnly<T>
where T: serde::Deserializer<'de> {
type Value = String;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("string")
}
fn visit_str<E>(self, value: &str) -> Result<String, E> {
Ok(String::from(value))
}
}
let s = &d.deserialize_string(StringOnly::<D>(PhantomData))?;

#if_patterns

static ALLOWED: &'static[&str] = &[#allowed];
Err(D::Error::unknown_variant(&s, ALLOWED))
}
}
}
}
5 changes: 5 additions & 0 deletions config_proc_macro/src/item_struct.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
use proc_macro2::TokenStream;

pub fn define_config_type_on_struct(_st: &syn::ItemStruct) -> syn::Result<TokenStream> {
unimplemented!()
}

0 comments on commit d3ec460

Please sign in to comment.