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"));