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 let parsed = match parser::parse(&input) {
19 Ok(p) => p,
20 Err(e) => return TokenStream::from(e.to_compile_error()),
21 };
22
23 let rels = relationships::generate(&parsed);
25
26 let builder_code = builder::generate(
28 &parsed,
29 &rels.flags,
30 &rels.inits,
31 &rels.methods,
32 &rels.eager_loads,
33 );
34
35 let factory_observer_code = factory_observer::generate(&parsed);
37
38 let model_code = models::generate(&parsed, &rels.model_methods);
40
41 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 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 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 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}