impl_table 0.1.3

Generate table binding and utils for rust-postgres and rusqlite.
Documentation
//! Generate table binding and utils for rust-postgres and rusqlite.
//!
//! # Example
//!
//! ```rust
//! extern crate chrono;
//!
//! use chrono::{DateTime, NaiveDate, TimeZone, Utc};
//! use impl_table::{impl_table, Table};
//!
//! // Optionally generate an id column and two timestamp columns: created_at and
//! // updated_at.
//! #[impl_table(name = "books", adaptor = rusqlite, with_columns(id, timestamps))]
//! #[derive(Table)]
//! struct Book {
//!     #[column] pub name: String,
//!     #[column] published_at: NaiveDate,
//!     #[column(name = "author_name")] author: String,
//! }
//!
//! let book = Book {
//!     id: 1,
//!     name: "The Man in the High Castle".into(),
//!     published_at: NaiveDate::from_ymd(1962, 10, 1),
//!     author: "Philip K. Dick".into(),
//!
//!     created_at: Utc.ymd(2019, 5, 22).and_hms(8, 0, 0),
//!     updated_at: Utc.ymd(2019, 5, 22).and_hms(8, 0, 0),
//! };
//! ```
//!
//! The above code generates an implementation like the following:
//!
//! ```rust
//! extern crate chrono;
//!
//! use chrono::{DateTime, NaiveDate, Utc};
//!
//! struct Book {
//!     id: i64,
//!     pub name: String,
//!     published_at: NaiveDate,
//!     author: i64,
//!
//!     created_at: DateTime<Utc>,
//!     updated_at: DateTime<Utc>,
//! }
//!
//! impl Book {
//!     pub const TABLE_NAME: &'static str = "books";
//!     pub const ADAPTOR_NAME: &'static str = "rusqlite";
//!
//!     fn table_name() -> &'static str {
//!         Self::TABLE_NAME
//!     }
//!
//!     fn all_columns() -> &'static [&'static str] {
//!         &["id", "name", "published_at", "author_name", "created_at", "updated_at"]
//!     }
//!
//!     fn from_row(row: &rusqlite::Row) -> rusqlite::Result<Self> {
//!         Ok(Self {
//!             id: row.get(0)?,
//!             name: row.get(1)?,
//!             published_at: row.get(2)?,
//!             author: row.get(3)?,
//!             created_at: row.get(4)?,
//!             updated_at: row.get(5)?,
//!         })
//!     }
//! }
//! ```
//!
//! For more examples see `test/sample.rs`.
//!
extern crate proc_macro;
extern crate proc_macro2;

mod derive_table;
mod parse_arguments;

use parse_arguments::parse_arguments;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};

/// Derive database table bindings and utils, e.g. `table_name()` and `all_columns()`.
#[proc_macro_derive(Table, attributes(column, primary_key))]
pub fn derive_table(item: TokenStream) -> TokenStream {
    match derive_table::derive_table(item) {
        Ok(tts) => tts.into(),
        Err(e) => e.to_compile_error().into(),
    }
}

#[derive(Debug, PartialEq)]
pub(crate) enum Argument {
    Switch { name: String },
    Flag { key: String, value: String },
    Function { name: String, args: Vec<Argument> },
}

macro_rules! build_field {
    ($($body:tt)*) => {
        {
            let mut outer_fields: syn::FieldsNamed = syn::parse_quote! {
                {
                    $($body)*
                }
            };
            outer_fields.named.pop().unwrap().into_value()
        }
    }
}

macro_rules! return_error_with_msg {
    ($msg:expr) => {
        return syn::Error::new(proc_macro2::Span::call_site(), $msg)
            .to_compile_error()
            .into();
    };
}

/// Collecte information to be used to derive a table struct.
///
/// Options are:
/// * table name in database. Required.
/// * adaptor name. Only "rusqlite" is supported at the moment.
/// * generated fields. `id`, `created_at` and `updated_at` are supported.
///
/// See `test/sample.rs` for examples.
///
#[proc_macro_attribute]
pub fn impl_table(attr: TokenStream, item: TokenStream) -> TokenStream {
    let arguments;
    match parse_arguments(
        proc_macro2::TokenStream::from(attr),
        proc_macro2::Span::call_site(),
    ) {
        Ok(arg) => arguments = arg,
        Err(e) => return syn::Error::from(e).to_compile_error().into(),
    }

    let mut name = "".to_string();
    let mut adaptor = "rusqlite".to_string();
    let mut with_id = false;
    let mut with_created_at = false;
    let mut with_updated_at = false;
    for arg in arguments {
        match arg {
            Argument::Flag { key, value } => {
                if key == "name" {
                    name = value;
                } else if key == "adaptor" {
                    adaptor = value;
                } else {
                    return_error_with_msg!(format!("Unrecognized flag with key {}.", key));
                }
            }
            Argument::Switch { name } => {
                return_error_with_msg!(format!("Unrecognized switch {}.", name));
            }
            Argument::Function { name, args } => {
                if name == "with_columns" {
                    for arg in args {
                        if let Argument::Switch { name } = arg {
                            if name == "id" {
                                with_id = true;
                            } else if name == "created_at" {
                                with_created_at = true;
                            } else if name == "updated_at" {
                                with_updated_at = true;
                            } else if name == "timestamps" {
                                with_created_at = true;
                                with_updated_at = true;
                            } else {
                                return_error_with_msg!(format!("Unrecognized switch {}.", name));
                            }
                        } else {
                            return_error_with_msg!(format!(
                                "Unrecognized function argument {:?}.",
                                arg
                            ));
                        }
                    }
                } else {
                    return_error_with_msg!(format!("Unrecognized function {}.", name));
                }
            }
        }
    }
    if name.is_empty() {
        return_error_with_msg!("Table name must be specified and non-empty.")
    }

    // Now we start to parse the struct
    let mut struct_def = parse_macro_input!(item as DeriveInput);
    let data = &mut struct_def.data;
    if let syn::Data::Struct(data_struct) = data {
        let fields = &mut data_struct.fields;

        // struct can only have named fields.
        if let syn::Fields::Named(named_fields) = fields {
            if with_id {
                named_fields
                    .named
                    .insert(0usize, build_field! { #[primary_key] id: i64 });
            }
            if with_created_at {
                named_fields
                    .named
                    .push(build_field! { #[column] created_at: chrono::DateTime<chrono::Utc> });
            }
            if with_updated_at {
                named_fields
                    .named
                    .push(build_field! { #[column] updated_at: chrono::DateTime<chrono::Utc> });
            }
        } else {
            panic!("Expecting named fields within a struct.");
        }
    } else {
        return_error_with_msg!("impl_table can only be applied to structs.")
    }

    let struct_name = &struct_def.ident;
    let expr = quote! {
        #struct_def

        impl #struct_name {
            pub const TABLE_NAME: &'static str = #name;
            pub const ADAPTOR_NAME: &'static str = #adaptor;
        }
    };
    expr.into()
}

#[macro_use]
extern crate doc_comment;
doc_comment!(include_str!("../README.md"));