Skip to main content

rullst_orm_macros/
lib.rs

1extern crate proc_macro;
2
3use proc_macro::TokenStream;
4use syn::{DeriveInput, parse_macro_input};
5
6mod builder;
7mod factory_observer;
8mod models;
9mod parser;
10mod relationships;
11
12#[cfg_attr(test, mutants::skip)]
13#[proc_macro_derive(Orm, attributes(orm, sqlx))]
14pub fn rullst_macro(input: TokenStream) -> TokenStream {
15    let input = parse_macro_input!(input as DeriveInput);
16
17    // Parse the input
18    let parsed = match parser::parse(&input) {
19        Ok(p) => p,
20        Err(e) => return TokenStream::from(e.to_compile_error()),
21    };
22
23    // Generate relationships
24    let rels = relationships::generate(&parsed);
25
26    // Generate the builder
27    let builder_code = builder::generate(
28        &parsed,
29        &rels.flags,
30        &rels.inits,
31        &rels.methods,
32        &rels.eager_loads,
33    );
34
35    // Generate factory and observers
36    let factory_observer_code = factory_observer::generate(&parsed);
37
38    // Generate the model impl
39    let model_code = models::generate(&parsed, &rels.model_methods);
40
41    // Combine
42    let expanded = quote::quote! {
43        #builder_code
44        #factory_observer_code
45        #model_code
46    };
47
48    TokenStream::from(expanded)
49}
50
51#[cfg(test)]
52mod tests {
53    use super::*;
54    use syn::parse_quote;
55
56    fn run_macro_generator(input: &DeriveInput) -> (parser::ParsedModel, String, String) {
57        let parsed = parser::parse(input).unwrap();
58        let rels = relationships::generate(&parsed);
59        let builder = builder::generate(
60            &parsed,
61            &rels.flags,
62            &rels.inits,
63            &rels.methods,
64            &rels.eager_loads,
65        );
66        let _factory = factory_observer::generate(&parsed);
67        let models = models::generate(&parsed, &rels.model_methods);
68        (parsed, builder.to_string(), models.to_string())
69    }
70
71    #[test]
72    fn test_basic_model() {
73        let input: DeriveInput = parse_quote! {
74            #[derive(Orm)]
75            #[orm(table = "users")]
76            pub struct User {
77                pub id: i32,
78                pub name: String,
79                pub email: String,
80            }
81        };
82        let (parsed, builder, models) = run_macro_generator(&input);
83        assert_eq!(parsed.table_name, "users");
84        assert!(builder.contains("where_id"));
85        assert!(models.contains("fn delete"));
86        assert!(models.contains("fn search"));
87    }
88
89    #[test]
90    fn test_model_with_relations() {
91        let input: DeriveInput = parse_quote! {
92            #[derive(Orm)]
93            pub struct Post {
94                pub id: i32,
95                pub title: String,
96                #[orm(has_many = "Comment", foreign_key = "post_id", local_key = "id")]
97                comments: Option<Vec<Comment>>,
98                #[orm(has_one = "Author", foreign_key = "post_id", local_key = "id")]
99                author: Option<Author>,
100                #[orm(belongs_to = "User", foreign_key = "user_id", local_key = "id")]
101                user: Option<User>,
102                #[orm(belongs_to_many = "Tag", pivot_table = "post_tags", foreign_key = "post_id", related_key = "tag_id")]
103                tags: Option<Vec<Tag>>,
104                #[orm(morph_one = "Image", morph_name = "imageable")]
105                image: Option<Image>,
106                #[orm(morph_many = "Comment", morph_name = "commentable")]
107                morph_comments: Option<Vec<Comment>>,
108            }
109        };
110        let (parsed, _, _) = run_macro_generator(&input);
111        assert!(!parsed.relations.is_empty());
112    }
113
114    #[test]
115    fn test_model_with_soft_deletes() {
116        let input: DeriveInput = parse_quote! {
117            #[derive(Orm)]
118            pub struct User {
119                pub id: i32,
120                pub name: String,
121                pub deleted_at: Option<String>,
122            }
123        };
124        let (parsed, builder, _) = run_macro_generator(&input);
125        assert!(parsed.has_soft_deletes);
126        assert!(builder.contains("deleted_at IS NULL"));
127    }
128
129    #[test]
130    fn test_model_with_hidden_fields() {
131        let input: DeriveInput = parse_quote! {
132            #[derive(Orm)]
133            pub struct User {
134                pub id: i32,
135                pub name: String,
136                #[orm(hidden)]
137                pub password: String,
138            }
139        };
140        let (parsed, _, models) = run_macro_generator(&input);
141        assert_eq!(parsed.hidden_fields.len(), 1);
142        assert!(models.contains("password"));
143    }
144
145    #[test]
146    fn test_model_with_explicit_soft_delete_config() {
147        let input: DeriveInput = parse_quote! {
148            #[derive(Orm)]
149            #[orm(soft_delete(field = "is_deleted", value = "0", delval = "1"))]
150            pub struct Post {
151                pub id: i32,
152                pub title: String,
153                pub is_deleted: i32,
154            }
155        };
156        let (parsed, _, _) = run_macro_generator(&input);
157        assert!(parsed.has_soft_deletes);
158    }
159
160    #[test]
161    fn test_model_with_all_hooks_and_scopes() {
162        let input: DeriveInput = parse_quote! {
163            #[derive(Orm)]
164            #[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")]
165            pub struct User {
166                pub id: i32,
167            }
168        };
169        let (parsed, _, _) = run_macro_generator(&input);
170        assert_eq!(parsed.global_scope, "active");
171        assert_eq!(parsed.tenant_column, "account_id");
172        assert_eq!(parsed.before_save, "hash_pwd");
173        assert_eq!(parsed.after_save, "log_evt");
174        assert_eq!(parsed.before_delete, "check_perm");
175        assert_eq!(parsed.after_delete, "clear_cache");
176        assert_eq!(parsed.after_fetch, "decrypt_data");
177    }
178
179    #[test]
180    fn test_model_with_soft_delete_null_sentinel() {
181        let input: DeriveInput = parse_quote! {
182            #[derive(Orm)]
183            #[orm(soft_delete(field = "deleted_at", value = "null", delval = "now()"))]
184            pub struct Audit {
185                pub id: i32,
186                pub message: String,
187                pub deleted_at: Option<String>,
188            }
189        };
190        run_macro_generator(&input);
191    }
192
193    #[test]
194    fn test_model_with_soft_delete_bigint_timestamp() {
195        let input: DeriveInput = parse_quote! {
196            #[derive(Orm)]
197            #[orm(soft_delete(field = "deleted_at", value = "0", delval = "UNIX_TIMESTAMP()"))]
198            pub struct Article {
199                pub id: i32,
200                pub title: String,
201                pub deleted_at: i64,
202            }
203        };
204        run_macro_generator(&input);
205    }
206
207    #[test]
208    fn test_model_with_orm_skip_field() {
209        let input: DeriveInput = parse_quote! {
210            #[derive(Orm)]
211            pub struct Account {
212                pub id: i32,
213                pub name: String,
214                #[orm(skip)]
215                pub password_hash: String,
216            }
217        };
218        run_macro_generator(&input);
219    }
220
221    #[test]
222    fn test_model_with_sqlx_skip_field() {
223        let input: DeriveInput = parse_quote! {
224            #[derive(Orm)]
225            pub struct Account {
226                pub id: i32,
227                pub name: String,
228                #[sqlx(skip)]
229                pub password_hash: String,
230            }
231        };
232        run_macro_generator(&input);
233    }
234
235    #[test]
236    fn test_model_with_combined_soft_delete_and_skip() {
237        let input: DeriveInput = parse_quote! {
238            #[derive(Orm)]
239            #[orm(soft_delete(field = "is_active", value = "true", delval = "false"))]
240            pub struct User {
241                pub id: i32,
242                pub name: String,
243                pub is_active: bool,
244                #[sqlx(skip)]
245                pub internal_note: String,
246            }
247        };
248        run_macro_generator(&input);
249    }
250
251    #[test]
252    fn test_parser_errors() {
253        // lowercase relation model
254        let input: DeriveInput = parse_quote! {
255            #[derive(Orm)]
256            pub struct Post {
257                pub id: i32,
258                #[orm(has_many = "comment")]
259                comments: Option<Vec<Comment>>,
260            }
261        };
262        let res = parser::parse(&input);
263        println!(
264            "PARSE RESULT FOR LOWERCASE RELATION: {:?}",
265            res.as_ref().map(|p| &p.table_name)
266        );
267        assert!(res.is_err());
268
269        // empty table name
270        let input: DeriveInput = parse_quote! {
271            #[derive(Orm)]
272            #[orm(table = "")]
273            pub struct Post {
274                pub id: i32,
275            }
276        };
277        assert!(parser::parse(&input).is_err());
278
279        // empty has_many
280        let input: DeriveInput = parse_quote! {
281            #[derive(Orm)]
282            pub struct Post {
283                pub id: i32,
284                #[orm(has_many = "")]
285                comments: Option<Vec<Comment>>,
286            }
287        };
288        assert!(parser::parse(&input).is_err());
289    }
290}