Skip to content

Commit 60ae167

Browse files
committed
Re-implement #[derive(Associations)] using Macros 1.1
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. We're cloning the attributes, as `quote!` only works for repetition with `T where for<'a> &'a T: IntoIterator`. I think this is a bug and will address upstream.
1 parent 1dfaa87 commit 60ae167

5 files changed

Lines changed: 119 additions & 9 deletions

File tree

diesel_codegen/src/associations.rs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
use syn;
2+
use quote;
3+
4+
use model::{Model, infer_association_name};
5+
use util::str_value_of_meta_item;
6+
7+
pub fn derive_associations(input: syn::MacroInput) -> quote::Tokens {
8+
let mut derived_associations = Vec::new();
9+
let model = t!(Model::from_item(&input, "Associations"));
10+
11+
for attr in &input.attrs {
12+
if attr.name() == "has_many" {
13+
let options = t!(build_association_options(attr, "has_many"));
14+
derived_associations.push(expand_has_many(&model, options))
15+
}
16+
17+
if attr.name() == "belongs_to" {
18+
let options = t!(build_association_options(attr, "belongs_to"));
19+
derived_associations.push(expand_belongs_to(&model, options))
20+
}
21+
}
22+
23+
quote!(#(derived_associations)*)
24+
}
25+
26+
fn expand_belongs_to(model: &Model, options: AssociationOptions) -> quote::Tokens {
27+
let parent_struct = options.name;
28+
let struct_name = &model.name;
29+
30+
let foreign_key_name = options.foreign_key_name.unwrap_or_else(||
31+
to_foreign_key(&parent_struct.as_ref()));
32+
let child_table_name = model.table_name();
33+
let fields = &model.attrs;
34+
35+
quote!(BelongsTo! {
36+
(
37+
struct_name = #struct_name,
38+
parent_struct = #parent_struct,
39+
foreign_key_name = #foreign_key_name,
40+
child_table_name = #child_table_name,
41+
),
42+
fields = [#(fields)*],
43+
})
44+
}
45+
46+
fn expand_has_many(model: &Model, options: AssociationOptions) -> quote::Tokens {
47+
let parent_table_name = model.table_name();
48+
let child_table_name = options.name;
49+
let foreign_key_name = options.foreign_key_name.unwrap_or_else(||
50+
to_foreign_key(&model.name.as_ref()));
51+
let fields = &model.attrs;
52+
53+
quote!(HasMany! {
54+
(
55+
parent_table_name = #parent_table_name,
56+
child_table = #child_table_name::table,
57+
foreign_key = #child_table_name::#foreign_key_name,
58+
),
59+
fields = [#(fields)*],
60+
})
61+
}
62+
63+
struct AssociationOptions {
64+
name: syn::Ident,
65+
foreign_key_name: Option<syn::Ident>,
66+
}
67+
68+
fn build_association_options(
69+
attr: &syn::Attribute,
70+
association_kind: &str,
71+
) -> Result<AssociationOptions, String> {
72+
let usage_error = Err(format!(
73+
"`#[{}]` must be in the form `#[{}(table_name, option=value)]`",
74+
association_kind, association_kind));
75+
match attr.value {
76+
syn::MetaItem::List(_, ref options) if options.len() >= 1 => {
77+
let association_name = match options[0] {
78+
syn::MetaItem::Word(ref name) => name.clone(),
79+
_ => return usage_error,
80+
};
81+
let foreign_key_name = options.iter().find(|a| a.name() == "foreign_key")
82+
.map(|a| str_value_of_meta_item(a, "foreign_key"))
83+
.map(syn::Ident::new);
84+
85+
Ok(AssociationOptions {
86+
name: association_name,
87+
foreign_key_name: foreign_key_name,
88+
})
89+
}
90+
_ => usage_error,
91+
}
92+
}
93+
94+
fn to_foreign_key(model_name: &str) -> syn::Ident {
95+
let lower_cased = infer_association_name(model_name);
96+
syn::Ident::new(format!("{}_id", &lower_cased))
97+
}
98+
99+
#[test]
100+
fn to_foreign_key_properly_handles_underscores() {
101+
assert_eq!(str_to_ident("foo_bar_id"), to_foreign_key("FooBar"));
102+
assert_eq!(str_to_ident("foo_bar_baz_id"), to_foreign_key("FooBarBaz"));
103+
}

diesel_codegen/src/lib.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ extern crate rustc_macro;
1616
extern crate syn;
1717

1818
mod as_changeset;
19+
mod associations;
1920
mod ast_builder;
2021
mod attr;
2122
mod identifiable;
@@ -31,14 +32,17 @@ use self::util::{list_value_of_attr_with_name, strip_attributes, strip_field_att
3132

3233
const KNOWN_CUSTOM_DERIVES: &'static [&'static str] = &[
3334
"AsChangeset",
35+
"Associations",
3436
"Identifiable",
3537
"Insertable",
3638
"Queryable",
3739
];
3840

3941
const KNOWN_CUSTOM_ATTRIBUTES: &'static [&'static str] = &[
42+
"belongs_to",
43+
"changeset_options",
44+
"has_many",
4045
"table_name",
41-
"changeset_options"
4246
];
4347

4448
const KNOWN_FIELD_ATTRIBUTES: &'static [&'static str] = &[
@@ -65,6 +69,11 @@ pub fn derive_as_changeset(input: TokenStream) -> TokenStream {
6569
expand_derive(input, as_changeset::derive_as_changeset)
6670
}
6771

72+
#[rustc_macro_derive(Associations)]
73+
pub fn derive_associations(input: TokenStream) -> TokenStream {
74+
expand_derive(input, associations::derive_associations)
75+
}
76+
6877
fn expand_derive(input: TokenStream, f: fn(syn::MacroInput) -> quote::Tokens) -> TokenStream {
6978
let mut item = parse_macro_input(&input.to_string()).unwrap();
7079
let output = f(item.clone());

diesel_codegen/src/util.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,11 @@ pub fn attr_with_name<'a>(
5454
}
5555

5656
fn str_value_of_attr<'a>(attr: &'a Attribute, name: &str) -> &'a str {
57-
match attr.value {
57+
str_value_of_meta_item(&attr.value, name)
58+
}
59+
60+
pub fn str_value_of_meta_item<'a>(item: &'a MetaItem, name: &str) -> &'a str {
61+
match *item {
5862
MetaItem::NameValue(_, Lit::Str(ref value, _)) => &*value,
5963
_ => panic!(r#"`{}` must be in the form `#[{}="something"]`"#, name, name),
6064
}

diesel_codegen_old/src/lib.rs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,6 @@ use diesel_codegen_syntex::*;
88

99
#[plugin_registrar]
1010
pub fn register(reg: &mut rustc_plugin::Registry) {
11-
use syntax::parse::token::intern;
12-
use syntax::ext::base::MultiDecorator;
13-
reg.register_syntax_extension(
14-
intern("derive_Associations"),
15-
MultiDecorator(Box::new(associations::expand_derive_associations)),
16-
);
1711
reg.register_macro("embed_migrations", migrations::expand_embed_migrations);
1812
reg.register_macro("infer_table_from_schema", schema_inference::expand_load_table);
1913
reg.register_macro("infer_schema", schema_inference::expand_infer_schema);

diesel_tests/tests/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#![cfg_attr(feature = "unstable", feature(custom_derive, plugin, custom_attribute, rustc_macro))]
1+
#![cfg_attr(feature = "unstable", feature(plugin, rustc_macro))]
22
#![cfg_attr(feature = "unstable", plugin(diesel_codegen_old, dotenv_macros))]
33

44
extern crate quickcheck;

0 commit comments

Comments
 (0)