impl_table/
lib.rs

1//! Generate table binding and utils for rust-postgres and rusqlite.
2//!
3//! # Example
4//!
5//! ```rust
6//! extern crate chrono;
7//!
8//! use chrono::{DateTime, NaiveDate, TimeZone, Utc};
9//! use impl_table::{impl_table, Table};
10//!
11//! // Optionally generate an id column and two timestamp columns: created_at and
12//! // updated_at.
13//! #[impl_table(name = "books", adaptor = rusqlite, with_columns(id, timestamps))]
14//! #[derive(Table)]
15//! struct Book {
16//!     #[column] pub name: String,
17//!     #[column] published_at: NaiveDate,
18//!     #[column(name = "author_name")] author: String,
19//! }
20//!
21//! let book = Book {
22//!     id: 1,
23//!     name: "The Man in the High Castle".into(),
24//!     published_at: NaiveDate::from_ymd(1962, 10, 1),
25//!     author: "Philip K. Dick".into(),
26//!
27//!     created_at: Utc.ymd(2019, 5, 22).and_hms(8, 0, 0),
28//!     updated_at: Utc.ymd(2019, 5, 22).and_hms(8, 0, 0),
29//! };
30//! ```
31//!
32//! The above code generates an implementation like the following:
33//!
34//! ```rust
35//! extern crate chrono;
36//!
37//! use chrono::{DateTime, NaiveDate, Utc};
38//!
39//! struct Book {
40//!     id: i64,
41//!     pub name: String,
42//!     published_at: NaiveDate,
43//!     author: i64,
44//!
45//!     created_at: DateTime<Utc>,
46//!     updated_at: DateTime<Utc>,
47//! }
48//!
49//! impl Book {
50//!     pub const TABLE_NAME: &'static str = "books";
51//!     pub const ADAPTOR_NAME: &'static str = "rusqlite";
52//!
53//!     fn table_name() -> &'static str {
54//!         Self::TABLE_NAME
55//!     }
56//!
57//!     fn all_columns() -> &'static [&'static str] {
58//!         &["id", "name", "published_at", "author_name", "created_at", "updated_at"]
59//!     }
60//!
61//!     fn from_row(row: &rusqlite::Row) -> rusqlite::Result<Self> {
62//!         Ok(Self {
63//!             id: row.get(0)?,
64//!             name: row.get(1)?,
65//!             published_at: row.get(2)?,
66//!             author: row.get(3)?,
67//!             created_at: row.get(4)?,
68//!             updated_at: row.get(5)?,
69//!         })
70//!     }
71//! }
72//! ```
73//!
74//! For more examples see `test/sample.rs`.
75//!
76extern crate proc_macro;
77extern crate proc_macro2;
78
79mod derive_table;
80mod parse_arguments;
81
82use parse_arguments::parse_arguments;
83use proc_macro::TokenStream;
84use quote::quote;
85use syn::{parse_macro_input, DeriveInput};
86
87/// Derive database table bindings and utils, e.g. `table_name()` and `all_columns()`.
88#[proc_macro_derive(Table, attributes(column, primary_key))]
89pub fn derive_table(item: TokenStream) -> TokenStream {
90    match derive_table::derive_table(item) {
91        Ok(tts) => tts.into(),
92        Err(e) => e.to_compile_error().into(),
93    }
94}
95
96#[derive(Debug, PartialEq)]
97pub(crate) enum Argument {
98    Switch { name: String },
99    Flag { key: String, value: String },
100    Function { name: String, args: Vec<Argument> },
101}
102
103macro_rules! build_field {
104    ($($body:tt)*) => {
105        {
106            let mut outer_fields: syn::FieldsNamed = syn::parse_quote! {
107                {
108                    $($body)*
109                }
110            };
111            outer_fields.named.pop().unwrap().into_value()
112        }
113    }
114}
115
116macro_rules! return_error_with_msg {
117    ($msg:expr) => {
118        return syn::Error::new(proc_macro2::Span::call_site(), $msg)
119            .to_compile_error()
120            .into();
121    };
122}
123
124/// Collecte information to be used to derive a table struct.
125///
126/// Options are:
127/// * table name in database. Required.
128/// * adaptor name. Only "rusqlite" is supported at the moment.
129/// * generated fields. `id`, `created_at` and `updated_at` are supported.
130///
131/// See `test/sample.rs` for examples.
132///
133#[proc_macro_attribute]
134pub fn impl_table(attr: TokenStream, item: TokenStream) -> TokenStream {
135    let arguments;
136    match parse_arguments(
137        proc_macro2::TokenStream::from(attr),
138        proc_macro2::Span::call_site(),
139    ) {
140        Ok(arg) => arguments = arg,
141        Err(e) => return syn::Error::from(e).to_compile_error().into(),
142    }
143
144    let mut name = "".to_string();
145    let mut adaptor = "rusqlite".to_string();
146    let mut with_id = false;
147    let mut with_created_at = false;
148    let mut with_updated_at = false;
149    for arg in arguments {
150        match arg {
151            Argument::Flag { key, value } => {
152                if key == "name" {
153                    name = value;
154                } else if key == "adaptor" {
155                    adaptor = value;
156                } else {
157                    return_error_with_msg!(format!("Unrecognized flag with key {}.", key));
158                }
159            }
160            Argument::Switch { name } => {
161                return_error_with_msg!(format!("Unrecognized switch {}.", name));
162            }
163            Argument::Function { name, args } => {
164                if name == "with_columns" {
165                    for arg in args {
166                        if let Argument::Switch { name } = arg {
167                            if name == "id" {
168                                with_id = true;
169                            } else if name == "created_at" {
170                                with_created_at = true;
171                            } else if name == "updated_at" {
172                                with_updated_at = true;
173                            } else if name == "timestamps" {
174                                with_created_at = true;
175                                with_updated_at = true;
176                            } else {
177                                return_error_with_msg!(format!("Unrecognized switch {}.", name));
178                            }
179                        } else {
180                            return_error_with_msg!(format!(
181                                "Unrecognized function argument {:?}.",
182                                arg
183                            ));
184                        }
185                    }
186                } else {
187                    return_error_with_msg!(format!("Unrecognized function {}.", name));
188                }
189            }
190        }
191    }
192    if name.is_empty() {
193        return_error_with_msg!("Table name must be specified and non-empty.")
194    }
195
196    // Now we start to parse the struct
197    let mut struct_def = parse_macro_input!(item as DeriveInput);
198    let data = &mut struct_def.data;
199    if let syn::Data::Struct(data_struct) = data {
200        let fields = &mut data_struct.fields;
201
202        // struct can only have named fields.
203        if let syn::Fields::Named(named_fields) = fields {
204            if with_id {
205                named_fields
206                    .named
207                    .insert(0usize, build_field! { #[primary_key] id: i64 });
208            }
209            if with_created_at {
210                named_fields
211                    .named
212                    .push(build_field! { #[column] created_at: chrono::DateTime<chrono::Utc> });
213            }
214            if with_updated_at {
215                named_fields
216                    .named
217                    .push(build_field! { #[column] updated_at: chrono::DateTime<chrono::Utc> });
218            }
219        } else {
220            panic!("Expecting named fields within a struct.");
221        }
222    } else {
223        return_error_with_msg!("impl_table can only be applied to structs.")
224    }
225
226    let struct_name = &struct_def.ident;
227    let expr = quote! {
228        #struct_def
229
230        impl #struct_name {
231            pub const TABLE_NAME: &'static str = #name;
232            pub const ADAPTOR_NAME: &'static str = #adaptor;
233        }
234    };
235    expr.into()
236}
237
238#[macro_use]
239extern crate doc_comment;
240doc_comment!(include_str!("../README.md"));