conform_derive/
lib.rs

1#![recursion_limit = "128"]
2
3extern crate proc_macro;
4#[macro_use]
5extern crate syn;
6#[macro_use]
7extern crate quote;
8extern crate conform;
9
10use proc_macro::TokenStream;
11use quote::ToTokens;
12
13#[proc_macro_derive(Conform, attributes(conform))]
14pub fn derive_conform(input: TokenStream) -> TokenStream {
15  let ast = syn::parse(input).unwrap();
16  impl_conform(&ast).into()
17}
18
19fn impl_conform(ast: &syn::DeriveInput) -> quote::Tokens {
20  let fields: Vec<syn::Field> = match ast.data {
21    syn::Data::Struct(syn::DataStruct { ref fields, .. }) => {
22      if fields.iter().any(|field| field.ident.is_none()) {
23        panic!("struct has unnamed fields");
24      }
25      fields.iter().cloned().collect()
26    }
27    _ => panic!("#[derive(Conform)] is only defined for structs"),
28  };
29
30  let mut tokens = vec![];
31
32  for field in &fields {
33    for attr in &field.attrs {
34      if attr.path != parse_quote!(conform) {
35        continue;
36      }
37
38      let field_ident = field.ident.clone().unwrap();
39
40      match attr.interpret_meta() {
41        Some(syn::Meta::List(syn::MetaList { ref nested, .. })) => {
42          let meta_items: Vec<&syn::NestedMeta> = nested.iter().collect();
43          for meta_item in meta_items {
44            match *meta_item {
45              syn::NestedMeta::Meta(ref item) => match *item {
46                syn::Meta::Word(ref name) => match name.to_string().as_ref() {
47                  "trim" => tokens.push(field_tokens(
48                    field,
49                    field_ident,
50                    quote!(conform::make::trim),
51                  )),
52                  "trim_left" => tokens.push(field_tokens(
53                    field,
54                    field_ident,
55                    quote!(conform::make::trim_left),
56                  )),
57                  "trim_right" => tokens.push(field_tokens(
58                    field,
59                    field_ident,
60                    quote!(conform::make::trim_right),
61                  )),
62                  "lower" => tokens.push(field_tokens(
63                    field,
64                    field_ident,
65                    quote!(conform::make::lower),
66                  )),
67                  "upper" => tokens.push(field_tokens(
68                    field,
69                    field_ident,
70                    quote!(conform::make::upper),
71                  )),
72                  "sentence" => tokens.push(field_tokens(
73                    field,
74                    field_ident,
75                    quote!(conform::make::sentence),
76                  )),
77                  "title" => tokens.push(field_tokens(
78                    field,
79                    field_ident,
80                    quote!(conform::make::title),
81                  )),
82                  "camel" => tokens.push(field_tokens(
83                    field,
84                    field_ident,
85                    quote!(conform::make::camel),
86                  )),
87                  "pascal" => tokens.push(field_tokens(
88                    field,
89                    field_ident,
90                    quote!(conform::make::pascal),
91                  )),
92                  "kebab" => tokens.push(field_tokens(
93                    field,
94                    field_ident,
95                    quote!(conform::make::kebab),
96                  )),
97                  "train" => tokens.push(field_tokens(
98                    field,
99                    field_ident,
100                    quote!(conform::make::train),
101                  )),
102                  "snake" => tokens.push(field_tokens(
103                    field,
104                    field_ident,
105                    quote!(conform::make::snake),
106                  )),
107                  "constant" => tokens.push(field_tokens(
108                    field,
109                    field_ident,
110                    quote!(conform::make::constant),
111                  )),
112                  _ => panic!("Unexpected conform argument: {}", name),
113                },
114                _ => unreachable!("Found a non Word while looking for conform arguments"),
115              },
116              _ => unreachable!("Found a non Meta while looking for conform arguments"),
117            }
118          }
119        }
120        _ => unreachable!(
121          "Got something other than a list of attributes while checking field `{}`",
122          field_ident
123        ),
124      }
125    }
126  }
127
128  let ident = &ast.ident;
129  let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
130
131  quote! {
132    impl #impl_generics Conform for #ident #ty_generics #where_clause {
133      fn conform(&mut self) -> &mut Self {
134        use conform;
135        #(#tokens)*
136        self
137      }
138    }
139  }
140}
141
142fn field_tokens(
143  field: &syn::Field,
144  field_ident: syn::Ident,
145  transform: quote::Tokens,
146) -> quote::Tokens {
147  match field.ty {
148    syn::Type::Path(syn::TypePath { ref path, .. }) => {
149      let mut tokens = quote::Tokens::new();
150      path.to_tokens(&mut tokens);
151      match tokens.to_string().replace(' ', "").as_ref() {
152        "String" => {
153          quote! {
154            self.#field_ident = #transform(&self.#field_ident);
155          }
156        }
157        "Option<String>" => {
158          quote! {
159            self.#field_ident = match self.#field_ident {
160              Some(ref value) => Some(#transform(value)),
161              None => None
162            };
163          }
164        }
165        _ => panic!(
166          "Field `{}`: only `String` & `Option<String>` types are supported",
167          field_ident
168        ),
169      }
170    }
171    _ => panic!(
172      "Field `{}`: only `String` & `Option<String>` types are supported",
173      field_ident
174    ),
175  }
176}