1mod attribute_parser;
8mod codegen;
9mod fields;
10mod macro_implementation;
11mod relation_validator;
12mod traits;
13
14use proc_macro::TokenStream;
15use quote::{format_ident, quote};
16use syn::{DeriveInput, parse_macro_input};
17use traits::crudresource::structs::CRUDResourceMeta;
18
19fn extract_active_model_type(
20 input: &DeriveInput,
21 name: &syn::Ident,
22) -> Result<proc_macro2::TokenStream, proc_macro2::TokenStream> {
23 for attr in &input.attrs {
24 if attr.path().is_ident("active_model")
25 && let Some(s) = attribute_parser::get_string_from_attr(attr)
26 {
27 return match syn::parse_str::<syn::Type>(&s) {
28 Ok(ty) => Ok(quote! { #ty }),
29 Err(_) => Err(syn::Error::new_spanned(
30 attr,
31 format!("Invalid active_model type: '{s}'. Expected a valid Rust type path."),
32 )
33 .to_compile_error()),
34 };
35 }
36 }
37 let ident = format_ident!("{}ActiveModel", name);
38 Ok(quote! { #ident })
39}
40
41
42#[proc_macro_derive(ToCreateModel, attributes(crudcrate, active_model))]
46pub fn to_create_model(input: TokenStream) -> TokenStream {
47 let input = parse_macro_input!(input as DeriveInput);
48 let name = &input.ident;
49 let create_name = format_ident!("{}Create", name);
50
51 let active_model_type = match extract_active_model_type(&input, name) {
52 Ok(ty) => ty,
53 Err(e) => return e.into(),
54 };
55 let fields = match fields::extract_named_fields(&input) {
56 Ok(f) => f,
57 Err(e) => return e,
58 };
59 let create_struct_fields = codegen::models::create::generate_create_struct_fields(&fields);
60 let conv_lines = codegen::models::create::generate_create_conversion_lines(&fields);
61
62 let create_derives =
65 quote! { Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, utoipa::ToSchema };
66
67 let expanded = quote! {
68 #[derive(#create_derives)]
69 pub struct #create_name {
70 #(#create_struct_fields),*
71 }
72
73 impl From<#create_name> for #active_model_type {
74 fn from(create: #create_name) -> Self {
75 #active_model_type {
76 #(#conv_lines),*
77 }
78 }
79 }
80 };
81
82 TokenStream::from(expanded)
83}
84
85#[proc_macro_derive(ToUpdateModel, attributes(crudcrate, active_model))]
89pub fn to_update_model(input: TokenStream) -> TokenStream {
90 let input = parse_macro_input!(input as DeriveInput);
91 let name = &input.ident;
92 let update_name = format_ident!("{}Update", name);
93
94 let active_model_type = match extract_active_model_type(&input, name) {
95 Ok(ty) => ty,
96 Err(e) => return e.into(),
97 };
98 let fields = match fields::extract_named_fields(&input) {
99 Ok(f) => f,
100 Err(e) => return e,
101 };
102 let included_fields = crate::codegen::models::update::filter_update_fields(&fields);
103 let update_struct_fields =
104 crate::codegen::models::update::generate_update_struct_fields(&included_fields);
105 let included_merge = codegen::models::merge::generate_included_merge_code(&included_fields);
106 let excluded_merge = codegen::models::merge::generate_excluded_merge_code(&fields);
107
108 let update_derives =
111 quote! { Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, utoipa::ToSchema };
112
113 let expanded = quote! {
114 #[derive(#update_derives)]
115 pub struct #update_name {
116 #(#update_struct_fields),*
117 }
118
119 impl #update_name {
120 pub fn merge_fields(self, mut model: #active_model_type) -> Result<#active_model_type, crudcrate::ApiError> {
121 #(#included_merge)*
122 #(#excluded_merge)*
123 Ok(model)
124 }
125 }
126
127 impl crudcrate::traits::MergeIntoActiveModel<#active_model_type> for #update_name {
128 fn merge_into_activemodel(self, model: #active_model_type) -> Result<#active_model_type, crudcrate::ApiError> {
129 Self::merge_fields(self, model)
130 }
131 }
132 };
133
134 TokenStream::from(expanded)
135}
136
137#[proc_macro_derive(ToListModel, attributes(crudcrate))]
141pub fn to_list_model(input: TokenStream) -> TokenStream {
142 let input = parse_macro_input!(input as DeriveInput);
143 let name = &input.ident;
144 let list_name = format_ident!("{}List", name);
145
146 let fields = match fields::extract_named_fields(&input) {
147 Ok(f) => f,
148 Err(e) => return e,
149 };
150 let list_struct_fields = crate::codegen::models::list::generate_list_struct_fields(&fields);
151 let list_from_assignments =
152 crate::codegen::models::list::generate_list_from_assignments(&fields);
153
154 let list_derives = quote! { Clone, serde::Serialize, serde::Deserialize, utoipa::ToSchema };
157
158 let expanded = quote! {
159 #[derive(#list_derives)]
160 pub struct #list_name {
161 #(#list_struct_fields),*
162 }
163
164 impl From<#name> for #list_name {
165 fn from(model: #name) -> Self {
166 Self {
167 #(#list_from_assignments),*
168 }
169 }
170 }
171 };
172
173 TokenStream::from(expanded)
174}
175
176#[proc_macro_derive(EntityToModels, attributes(crudcrate))]
190pub fn entity_to_models(input: TokenStream) -> TokenStream {
191 let input = parse_macro_input!(input as DeriveInput);
192 let struct_name = &input.ident;
193
194 let (api_struct_name, active_model_path) = fields::parse_entity_attributes(&input, struct_name);
196 let table_name = attribute_parser::extract_table_name(&input.attrs)
197 .unwrap_or_else(|| struct_name.to_string());
198 let meta = attribute_parser::parse_crud_resource_meta(&input.attrs);
199
200 if !meta.deprecation_errors.is_empty() {
202 let errors: proc_macro2::TokenStream = meta
203 .deprecation_errors
204 .iter()
205 .map(syn::Error::to_compile_error)
206 .collect();
207 return errors.into();
208 }
209
210 let crud_meta = meta.with_defaults(&table_name);
211
212 if syn::parse_str::<syn::Type>(&active_model_path).is_err() {
214 return syn::Error::new_spanned(
215 &input,
216 format!("Invalid active_model path: {active_model_path}"),
217 )
218 .to_compile_error()
219 .into();
220 }
221
222 let fields = match fields::extract_entity_fields(&input) {
224 Ok(f) => f,
225 Err(e) => return e,
226 };
227 let field_analysis = match fields::analyze_entity_fields(fields) {
228 Ok(a) => a,
229 Err(e) => return e,
230 };
231 if let Err(e) = fields::validate_field_analysis(&field_analysis) {
232 return e;
233 }
234
235 let cyclic_dependency_check = relation_validator::generate_cyclic_dependency_check(
237 &field_analysis,
238 &api_struct_name.to_string(),
239 );
240 if !cyclic_dependency_check.is_empty() {
241 return cyclic_dependency_check.into();
242 }
243
244 let (api_struct_fields, from_model_assignments) =
246 codegen::models::api_struct::generate_api_struct_content(&field_analysis, &api_struct_name);
247 let api_struct = codegen::models::api_struct::generate_api_struct(
248 &api_struct_name,
249 &api_struct_fields,
250 &active_model_path,
251 &crud_meta,
252 &field_analysis,
253 );
254 let from_impl = quote! {
255 impl From<#struct_name> for #api_struct_name {
256 fn from(model: #struct_name) -> Self {
257 Self {
258 #(#from_model_assignments),*
259 }
260 }
261 }
262 };
263
264 let has_crud_resource_fields = field_analysis.primary_key_field.is_some()
266 || !field_analysis.sortable_fields.is_empty()
267 || !field_analysis.filterable_fields.is_empty()
268 || !field_analysis.fulltext_fields.is_empty();
269
270 let crud_impl_inner = if has_crud_resource_fields {
271 macro_implementation::generate_crud_resource_impl(
272 &api_struct_name,
273 &crud_meta,
274 &active_model_path,
275 &field_analysis,
276 &table_name,
277 )
278 } else {
279 quote! {}
280 };
281
282 let router_impl = if crud_meta.generate_router && has_crud_resource_fields {
283 crate::codegen::router::axum::generate_router_impl(&api_struct_name)
284 } else {
285 quote! {}
286 };
287
288 let crud_impl = quote! {
289 #crud_impl_inner
290 #router_impl
291 };
292
293 let (list_model, response_model) =
295 codegen::models::list_response::generate_list_and_response_models(
296 &input,
297 &api_struct_name,
298 struct_name,
299 &field_analysis,
300 );
301
302 let expanded = quote! {
304 #api_struct
305 #from_impl
306 #crud_impl
307 #list_model
308 #response_model
309 };
310
311 TokenStream::from(expanded)
312}