From d449f582d74a9709ae274d7a3565cd610e6d6f69 Mon Sep 17 00:00:00 2001 From: Sean Griffin Date: Mon, 3 Oct 2016 12:45:02 -0400 Subject: [PATCH] Re-implement `#[derive(Identifiable)]` using Macros 1.1 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. --- diesel_codegen/src/identifiable.rs | 22 +++++++++ diesel_codegen/src/lib.rs | 30 +++++++++++- diesel_codegen/src/model.rs | 46 ++++++++++++++++++- diesel_codegen/src/util.rs | 39 ++++++++++++++++ diesel_codegen_old/src/lib.rs | 4 -- ...s_method_on_structs_without_primary_key.rs | 2 +- ...identifiable_requires_primary_key_field.rs | 8 ++-- 7 files changed, 140 insertions(+), 11 deletions(-) create mode 100644 diesel_codegen/src/identifiable.rs diff --git a/diesel_codegen/src/identifiable.rs b/diesel_codegen/src/identifiable.rs new file mode 100644 index 000000000000..1a2a45dfc756 --- /dev/null +++ b/diesel_codegen/src/identifiable.rs @@ -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)*], + }) +} diff --git a/diesel_codegen/src/lib.rs b/diesel_codegen/src/lib.rs index 25c53198d8d9..1771c7504766 100644 --- a/diesel_codegen/src/lib.rs +++ b/diesel_codegen/src/lib.rs @@ -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() } diff --git a/diesel_codegen/src/model.rs b/diesel_codegen/src/model.rs index c28363abc4eb..02c42a82de95 100644 --- a/diesel_codegen/src/model.rs +++ b/diesel_codegen/src/model.rs @@ -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, pub name: syn::Ident, pub generics: syn::Generics, + table_name_from_annotation: Option, } impl Model { @@ -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")); } diff --git a/diesel_codegen/src/util.rs b/diesel_codegen/src/util.rs index 27ffc0155bc8..0daf4adb943e 100644 --- a/diesel_codegen/src/util.rs +++ b/diesel_codegen/src/util.rs @@ -24,6 +24,13 @@ 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, @@ -31,6 +38,13 @@ pub fn ident_value_of_attr_with_name<'a>( 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> { + attr_with_name(attrs, name).map(|attr| list_value_of_attr(attr, name)) +} + pub fn attr_with_name<'a>( attrs: &'a [Attribute], name: &str, @@ -38,6 +52,13 @@ pub fn attr_with_name<'a>( 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 { @@ -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 { @@ -65,3 +98,9 @@ pub fn is_option_ty(ty: &Ty) -> bool { _ => false, } } + +pub fn strip_attributes(attrs: Vec, names_to_strip: &[&str]) -> Vec { + attrs.into_iter().filter(|attr| { + !names_to_strip.contains(&attr.name()) + }).collect() +} diff --git a/diesel_codegen_old/src/lib.rs b/diesel_codegen_old/src/lib.rs index 51e87ecb1e7f..76ca01f84652 100644 --- a/diesel_codegen_old/src/lib.rs +++ b/diesel_codegen_old/src/lib.rs @@ -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)) diff --git a/diesel_compile_tests/tests/compile-fail/codegen_does_not_add_save_changes_method_on_structs_without_primary_key.rs b/diesel_compile_tests/tests/compile-fail/codegen_does_not_add_save_changes_method_on_structs_without_primary_key.rs index e7950ef0f921..fc1412c81414 100644 --- a/diesel_compile_tests/tests/compile-fail/codegen_does_not_add_save_changes_method_on_structs_without_primary_key.rs +++ b/diesel_compile_tests/tests/compile-fail/codegen_does_not_add_save_changes_method_on_structs_without_primary_key.rs @@ -16,7 +16,7 @@ table! { } } -#[derive(Queryable, AsChangeset)] //~ WARNING +#[derive(Queryable, AsChangeset)] #[table_name = "users"] pub struct User { name: String, diff --git a/diesel_compile_tests/tests/compile-fail/identifiable_requires_primary_key_field.rs b/diesel_compile_tests/tests/compile-fail/identifiable_requires_primary_key_field.rs index 56c5c733c90d..63f3f3139387 100644 --- a/diesel_compile_tests/tests/compile-fail/identifiable_requires_primary_key_field.rs +++ b/diesel_compile_tests/tests/compile-fail/identifiable_requires_primary_key_field.rs @@ -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,