extern crate proc_macro;
use proc_macro::TokenStream;
use syn::{DeriveInput, parse_macro_input};
mod builder;
mod factory_observer;
mod models;
mod parser;
mod relationships;
#[cfg_attr(test, mutants::skip)]
#[proc_macro_derive(Orm, attributes(orm, sqlx))]
pub fn rullst_macro(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let parsed = match parser::parse(&input) {
Ok(p) => p,
Err(e) => return TokenStream::from(e.to_compile_error()),
};
let rels = relationships::generate(&parsed);
let builder_code = builder::generate(
&parsed,
&rels.flags,
&rels.inits,
&rels.methods,
&rels.eager_loads,
);
let factory_observer_code = factory_observer::generate(&parsed);
let model_code = models::generate(&parsed, &rels.model_methods);
let expanded = quote::quote! {
#builder_code
#factory_observer_code
#model_code
};
TokenStream::from(expanded)
}
#[cfg(test)]
mod tests {
use super::*;
use syn::parse_quote;
fn run_macro_generator(input: &DeriveInput) -> (parser::ParsedModel, String, String) {
let parsed = parser::parse(input).unwrap();
let rels = relationships::generate(&parsed);
let builder = builder::generate(
&parsed,
&rels.flags,
&rels.inits,
&rels.methods,
&rels.eager_loads,
);
let _factory = factory_observer::generate(&parsed);
let models = models::generate(&parsed, &rels.model_methods);
(parsed, builder.to_string(), models.to_string())
}
#[test]
fn test_basic_model() {
let input: DeriveInput = parse_quote! {
#[derive(Orm)]
#[orm(table = "users")]
pub struct User {
pub id: i32,
pub name: String,
pub email: String,
}
};
let (parsed, builder, models) = run_macro_generator(&input);
assert_eq!(parsed.table_name, "users");
assert!(builder.contains("where_id"));
assert!(models.contains("fn delete"));
assert!(models.contains("fn search"));
}
#[test]
fn test_model_with_relations() {
let input: DeriveInput = parse_quote! {
#[derive(Orm)]
pub struct Post {
pub id: i32,
pub title: String,
#[orm(has_many = "Comment", foreign_key = "post_id", local_key = "id")]
comments: Option<Vec<Comment>>,
#[orm(has_one = "Author", foreign_key = "post_id", local_key = "id")]
author: Option<Author>,
#[orm(belongs_to = "User", foreign_key = "user_id", local_key = "id")]
user: Option<User>,
#[orm(belongs_to_many = "Tag", pivot_table = "post_tags", foreign_key = "post_id", related_key = "tag_id")]
tags: Option<Vec<Tag>>,
#[orm(morph_one = "Image", morph_name = "imageable")]
image: Option<Image>,
#[orm(morph_many = "Comment", morph_name = "commentable")]
morph_comments: Option<Vec<Comment>>,
}
};
let (parsed, _, _) = run_macro_generator(&input);
assert!(!parsed.relations.is_empty());
}
#[test]
fn test_model_with_soft_deletes() {
let input: DeriveInput = parse_quote! {
#[derive(Orm)]
pub struct User {
pub id: i32,
pub name: String,
pub deleted_at: Option<String>,
}
};
let (parsed, builder, _) = run_macro_generator(&input);
assert!(parsed.has_soft_deletes);
assert!(builder.contains("deleted_at IS NULL"));
}
#[test]
fn test_model_with_hidden_fields() {
let input: DeriveInput = parse_quote! {
#[derive(Orm)]
pub struct User {
pub id: i32,
pub name: String,
#[orm(hidden)]
pub password: String,
}
};
let (parsed, _, models) = run_macro_generator(&input);
assert_eq!(parsed.hidden_fields.len(), 1);
assert!(models.contains("password"));
}
#[test]
fn test_model_with_explicit_soft_delete_config() {
let input: DeriveInput = parse_quote! {
#[derive(Orm)]
#[orm(soft_delete(field = "is_deleted", value = "0", delval = "1"))]
pub struct Post {
pub id: i32,
pub title: String,
pub is_deleted: i32,
}
};
let (parsed, _, _) = run_macro_generator(&input);
assert!(parsed.has_soft_deletes);
}
#[test]
fn test_model_with_all_hooks_and_scopes() {
let input: DeriveInput = parse_quote! {
#[derive(Orm)]
#[orm(global_scope = "active", tenant_column = "account_id", before_save = "hash_pwd", after_save = "log_evt", before_delete = "check_perm", after_delete = "clear_cache", after_fetch = "decrypt_data")]
pub struct User {
pub id: i32,
}
};
let (parsed, _, _) = run_macro_generator(&input);
assert_eq!(parsed.global_scope, "active");
assert_eq!(parsed.tenant_column, "account_id");
assert_eq!(parsed.before_save, "hash_pwd");
assert_eq!(parsed.after_save, "log_evt");
assert_eq!(parsed.before_delete, "check_perm");
assert_eq!(parsed.after_delete, "clear_cache");
assert_eq!(parsed.after_fetch, "decrypt_data");
}
#[test]
fn test_model_with_soft_delete_null_sentinel() {
let input: DeriveInput = parse_quote! {
#[derive(Orm)]
#[orm(soft_delete(field = "deleted_at", value = "null", delval = "now()"))]
pub struct Audit {
pub id: i32,
pub message: String,
pub deleted_at: Option<String>,
}
};
run_macro_generator(&input);
}
#[test]
fn test_model_with_soft_delete_bigint_timestamp() {
let input: DeriveInput = parse_quote! {
#[derive(Orm)]
#[orm(soft_delete(field = "deleted_at", value = "0", delval = "UNIX_TIMESTAMP()"))]
pub struct Article {
pub id: i32,
pub title: String,
pub deleted_at: i64,
}
};
run_macro_generator(&input);
}
#[test]
fn test_model_with_orm_skip_field() {
let input: DeriveInput = parse_quote! {
#[derive(Orm)]
pub struct Account {
pub id: i32,
pub name: String,
#[orm(skip)]
pub password_hash: String,
}
};
run_macro_generator(&input);
}
#[test]
fn test_model_with_sqlx_skip_field() {
let input: DeriveInput = parse_quote! {
#[derive(Orm)]
pub struct Account {
pub id: i32,
pub name: String,
#[sqlx(skip)]
pub password_hash: String,
}
};
run_macro_generator(&input);
}
#[test]
fn test_model_with_combined_soft_delete_and_skip() {
let input: DeriveInput = parse_quote! {
#[derive(Orm)]
#[orm(soft_delete(field = "is_active", value = "true", delval = "false"))]
pub struct User {
pub id: i32,
pub name: String,
pub is_active: bool,
#[sqlx(skip)]
pub internal_note: String,
}
};
run_macro_generator(&input);
}
#[test]
fn test_parser_errors() {
let input: DeriveInput = parse_quote! {
#[derive(Orm)]
pub struct Post {
pub id: i32,
#[orm(has_many = "comment")]
comments: Option<Vec<Comment>>,
}
};
let res = parser::parse(&input);
println!(
"PARSE RESULT FOR LOWERCASE RELATION: {:?}",
res.as_ref().map(|p| &p.table_name)
);
assert!(res.is_err());
let input: DeriveInput = parse_quote! {
#[derive(Orm)]
#[orm(table = "")]
pub struct Post {
pub id: i32,
}
};
assert!(parser::parse(&input).is_err());
let input: DeriveInput = parse_quote! {
#[derive(Orm)]
pub struct Post {
pub id: i32,
#[orm(has_many = "")]
comments: Option<Vec<Comment>>,
}
};
assert!(parser::parse(&input).is_err());
}
}