Skip to content

Commit

Permalink
Re-implement #[derive(Identifiable)] using Macros 1.1
Browse files Browse the repository at this point in the history
This was extracted from #453.

Going forward Macros 1.1 is the intended path of stabilization for
procedural macros, so `diesel_codegen` will need to be rewritten to use
it.

Much of the helper code around this is a direct port of the libsyntax
version of our code, rewritten to use `syn` instead.
  • Loading branch information
sgrif committed Oct 3, 2016
1 parent 3cfbf25 commit d449f58
Show file tree
Hide file tree
Showing 7 changed files with 140 additions and 11 deletions.
22 changes: 22 additions & 0 deletions diesel_codegen/src/identifiable.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
use quote::Tokens;
use syn;

use model::Model;

pub fn derive_identifiable(item: syn::MacroInput) -> Tokens {
let model = t!(Model::from_item(&item, "Identifiable"));
let table_name = model.table_name();
let struct_ty = &model.ty;
let fields = model.attrs;
if !fields.iter().any(|f| f.field_name == Some(syn::Ident::new("id"))) {
panic!("Could not find a field named `id` on `{}`", &model.name);
}

quote!(Identifiable! {
(
table_name = #table_name,
struct_ty = #struct_ty,
),
fields = [#(fields)*],
})
}
30 changes: 29 additions & 1 deletion diesel_codegen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,49 @@ extern crate syn;

mod ast_builder;
mod attr;
mod identifiable;
mod model;
mod queryable;
mod util;

use rustc_macro::TokenStream;
use syn::parse_macro_input;
use util::{list_value_of_attr_with_name, strip_attributes};

const KNOWN_CUSTOM_DERIVES: &'static [&'static str] = &[
"Identifiable",
"Queryable",
];

const KNOWN_CUSTOM_ATTRIBUTES: &'static [&'static str] = &[
"table_name",
];

#[rustc_macro_derive(Queryable)]
pub fn derive_queryable(input: TokenStream) -> TokenStream {
expand_derive(input, queryable::derive_queryable)
}

#[rustc_macro_derive(Identifiable)]
pub fn derive_identifiable(input: TokenStream) -> TokenStream {
expand_derive(input, identifiable::derive_identifiable)
}

fn expand_derive(input: TokenStream, f: fn(syn::MacroInput) -> quote::Tokens) -> TokenStream {
let item = parse_macro_input(&input.to_string()).unwrap();
let mut item = parse_macro_input(&input.to_string()).unwrap();
let output = f(item.clone());

let finished_deriving_diesel_traits = {
let remaining_derives = list_value_of_attr_with_name(&item.attrs, "derive");
!remaining_derives
.unwrap_or(Vec::new())
.iter()
.any(|trait_name| KNOWN_CUSTOM_DERIVES.contains(&trait_name.as_ref()))
};

if finished_deriving_diesel_traits {
item.attrs = strip_attributes(item.attrs, KNOWN_CUSTOM_ATTRIBUTES);
}

quote!(#item #output).to_string().parse().unwrap()
}
46 changes: 45 additions & 1 deletion diesel_codegen/src/model.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
use syn;

use attr::Attr;
use util::struct_ty;
use util::{struct_ty, str_value_of_attr_with_name};

pub struct Model {
pub ty: syn::Ty,
pub attrs: Vec<Attr>,
pub name: syn::Ident,
pub generics: syn::Generics,
table_name_from_annotation: Option<syn::Ident>,
}

impl Model {
Expand All @@ -21,12 +22,55 @@ impl Model {
let ty = struct_ty(item.ident.clone(), &item.generics);
let name = item.ident.clone();
let generics = item.generics.clone();
let table_name_from_annotation = str_value_of_attr_with_name(
&item.attrs, "table_name").map(syn::Ident::new);

Ok(Model {
ty: ty,
attrs: attrs,
name: name,
generics: generics,
table_name_from_annotation: table_name_from_annotation,
})
}

pub fn table_name(&self) -> syn::Ident {
self.table_name_from_annotation.clone().unwrap_or_else(|| {
syn::Ident::new(infer_table_name(self.name.as_ref()))
})
}
}

pub fn infer_association_name(name: &str) -> String {
let mut result = String::with_capacity(name.len());
result.push_str(&name[..1].to_lowercase());
for character in name[1..].chars() {
if character.is_uppercase() {
result.push('_');
for lowercase in character.to_lowercase() {
result.push(lowercase);
}
} else {
result.push(character);
}
}
result
}

fn infer_table_name(name: &str) -> String {
let mut result = infer_association_name(name);
result.push('s');
result
}

#[test]
fn infer_table_name_pluralizes_and_downcases() {
assert_eq!("foos", &infer_table_name("Foo"));
assert_eq!("bars", &infer_table_name("Bar"));
}

#[test]
fn infer_table_name_properly_handles_underscores() {
assert_eq!("foo_bars", &infer_table_name("FooBar"));
assert_eq!("foo_bar_bazs", &infer_table_name("FooBarBaz"));
}
39 changes: 39 additions & 0 deletions diesel_codegen/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,41 @@ pub fn struct_ty(name: Ident, generics: &Generics) -> Ty {
})
}

pub fn str_value_of_attr_with_name<'a>(
attrs: &'a [Attribute],
name: &str,
) -> Option<&'a str> {
attr_with_name(attrs, name).map(|attr| str_value_of_attr(attr, name))
}

pub fn ident_value_of_attr_with_name<'a>(
attrs: &'a [Attribute],
name: &str,
) -> Option<&'a Ident> {
attr_with_name(attrs, name).map(|attr| single_arg_value_of_attr(attr, name))
}

pub fn list_value_of_attr_with_name<'a>(
attrs: &'a [Attribute],
name: &str,
) -> Option<Vec<&'a Ident>> {
attr_with_name(attrs, name).map(|attr| list_value_of_attr(attr, name))
}

pub fn attr_with_name<'a>(
attrs: &'a [Attribute],
name: &str,
) -> Option<&'a Attribute> {
attrs.into_iter().find(|attr| attr.name() == name)
}

fn str_value_of_attr<'a>(attr: &'a Attribute, name: &str) -> &'a str {
match attr.value {
MetaItem::NameValue(_, Lit::Str(ref value, _)) => &*value,
_ => panic!(r#"`{}` must be in the form `#[{}="something"]`"#, name, name),
}
}

fn single_arg_value_of_attr<'a>(attr: &'a Attribute, name: &str) -> &'a Ident {
let usage_err = || panic!(r#"`{}` must be in the form `#[{}(something)]`"#, name, name);
match attr.value {
Expand All @@ -54,6 +75,18 @@ fn single_arg_value_of_attr<'a>(attr: &'a Attribute, name: &str) -> &'a Ident {
}
}

fn list_value_of_attr<'a>(attr: &'a Attribute, name: &str) -> Vec<&'a Ident> {
match attr.value {
MetaItem::List(_, ref items) => {
items.iter().map(|item| match *item {
MetaItem::Word(ref name) => name,
_ => panic!("`{}` must be in the form `#[{}(something, something_else)]`", name, name),
}).collect()
}
_ => panic!("`{}` must be in the form `#[{}(something, something_else)]`", name, name),
}
}

pub fn is_option_ty(ty: &Ty) -> bool {
let option_ident = Ident::new("Option");
match *ty {
Expand All @@ -65,3 +98,9 @@ pub fn is_option_ty(ty: &Ty) -> bool {
_ => false,
}
}

pub fn strip_attributes(attrs: Vec<Attribute>, names_to_strip: &[&str]) -> Vec<Attribute> {
attrs.into_iter().filter(|attr| {
!names_to_strip.contains(&attr.name())
}).collect()
}
4 changes: 0 additions & 4 deletions diesel_codegen_old/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,6 @@ pub fn register(reg: &mut rustc_plugin::Registry) {
intern("derive_AsChangeset"),
MultiDecorator(Box::new(update::expand_derive_as_changeset)),
);
reg.register_syntax_extension(
intern("derive_Identifiable"),
MultiDecorator(Box::new(identifiable::expand_derive_identifiable))
);
reg.register_syntax_extension(
intern("derive_Insertable"),
MultiDecorator(Box::new(insertable::expand_derive_insertable))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ table! {
}
}

#[derive(Queryable, AsChangeset)] //~ WARNING
#[derive(Queryable, AsChangeset)]
#[table_name = "users"]
pub struct User {
name: String,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
#![feature(custom_derive, plugin, custom_attribute, rustc_macro)]
#![plugin(diesel_codegen_old)]
#![feature(rustc_macro)]

#[macro_use]
extern crate diesel;
#[macro_use]
extern crate diesel_codegen;

#[derive(Identifiable)] //~ ERROR Could not find a field named `id` on `User`
//~^ WARNING
#[derive(Identifiable)]
//~^ ERROR custom derive attribute panicked
//~| HELP Could not find a field named `id` on `User`
pub struct User {
name: String,
hair_color: Option<String>,
Expand Down

0 comments on commit d449f58

Please sign in to comment.